@igstack/app-catalog-frontend-core 6.9.0 → 6.10.1
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.
- package/dist/esm/__tests__/modules/appCatalog/ui/detail/CommunitySection.feed.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/appCatalog/ui/detail/CommunitySection.help.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/appCatalog/ui/detail/CommunitySection.input.test.d.ts +0 -0
- package/dist/esm/__tests__/modules/appCatalog/ui/detail/composeComments.test.d.ts +1 -0
- package/dist/esm/__tests__/ui/detail/CommunityInput.test.d.ts +1 -0
- package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +8 -0
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js +1 -0
- package/dist/esm/modules/appCatalog/ui/catalog/AppDetailPanel.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +237 -218
- package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.d.ts +17 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js +187 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.d.ts +9 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js +91 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.d.ts +17 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js +328 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.d.ts +15 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js +26 -0
- package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.d.ts +8 -19
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js +49 -37
- package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js.map +1 -1
- package/dist/esm/modules/appCatalog/ui/detail/communityDraft.d.ts +30 -0
- package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js +50 -0
- package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/communityError.d.ts +6 -0
- package/dist/esm/modules/appCatalog/ui/detail/communityError.js +8 -0
- package/dist/esm/modules/appCatalog/ui/detail/communityError.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/composeComments.d.ts +27 -0
- package/dist/esm/modules/appCatalog/ui/detail/composeComments.js +66 -0
- package/dist/esm/modules/appCatalog/ui/detail/composeComments.js.map +1 -0
- package/dist/esm/modules/appCatalog/ui/detail/usePendingCount.d.ts +11 -0
- package/dist/esm/modules/appCatalog/ui/detail/usePendingCount.js +15 -0
- package/dist/esm/modules/appCatalog/ui/detail/usePendingCount.js.map +1 -0
- package/dist/esm/modules/extensions/features/resourceDetail.d.ts +38 -0
- package/dist/esm/modules/extensions/features/resourceDetail.js +4 -0
- package/dist/esm/modules/extensions/features/resourceDetail.js.map +1 -1
- package/dist/esm/modules/extensions/index.d.ts +1 -1
- package/dist/esm/modules/extensions/index.js.map +1 -1
- package/dist/index.css +501 -49
- package/package.json +4 -4
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +5 -2
- package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.enter.test.tsx +5 -2
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +9 -10
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.behindThisCard.test.tsx +402 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +8 -4
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +5 -2
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.knowMore.test.tsx +175 -0
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +5 -2
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +5 -2
- package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +5 -2
- package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.feed.test.tsx +281 -0
- package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.help.test.tsx +179 -0
- package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.input.test.tsx +423 -0
- package/src/__tests__/modules/appCatalog/ui/detail/composeComments.test.ts +170 -0
- package/src/__tests__/ui/detail/CommunityInput.test.tsx +39 -0
- package/src/index.css +501 -49
- package/src/modules/appCatalog/ui/detail/AppDetails.tsx +292 -289
- package/src/modules/appCatalog/ui/detail/CommentsFeed.tsx +254 -0
- package/src/modules/appCatalog/ui/detail/CommentsHelp.tsx +102 -0
- package/src/modules/appCatalog/ui/detail/CommunityInput.tsx +419 -0
- package/src/modules/appCatalog/ui/detail/CommunitySection.tsx +41 -0
- package/src/modules/appCatalog/ui/detail/DetailTabs.tsx +77 -71
- package/src/modules/appCatalog/ui/detail/communityDraft.ts +80 -0
- package/src/modules/appCatalog/ui/detail/communityError.ts +10 -0
- package/src/modules/appCatalog/ui/detail/composeComments.ts +103 -0
- package/src/modules/appCatalog/ui/detail/usePendingCount.ts +26 -0
- package/src/modules/extensions/features/resourceDetail.ts +28 -0
- package/src/modules/extensions/index.ts +2 -0
- package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.d.ts +0 -13
- package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js +0 -433
- package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js.map +0 -1
- package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.emptyState.test.tsx +0 -181
- package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.reviewStatus.test.tsx +0 -131
- package/src/modules/appCatalog/ui/detail/FeedbackSection.tsx +0 -605
- /package/dist/esm/__tests__/modules/appCatalog/ui/detail/{FeedbackSection.emptyState.test.d.ts → AppDetails.behindThisCard.test.d.ts} +0 -0
- /package/dist/esm/__tests__/modules/appCatalog/ui/detail/{FeedbackSection.reviewStatus.test.d.ts → AppDetails.knowMore.test.d.ts} +0 -0
|
@@ -3,17 +3,11 @@ import type {
|
|
|
3
3
|
SourceReference,
|
|
4
4
|
} from '@igstack/app-catalog-backend-core'
|
|
5
5
|
import { useSearch } from '@tanstack/react-router'
|
|
6
|
-
import {
|
|
6
|
+
import { ExternalLink, Plus, Trash2 } from 'lucide-react'
|
|
7
7
|
import React from 'react'
|
|
8
8
|
import { useHotkeys } from 'react-hotkeys-hook'
|
|
9
9
|
import { Badge } from '~/ui/badge'
|
|
10
10
|
import { Button } from '~/ui/button'
|
|
11
|
-
import {
|
|
12
|
-
Accordion,
|
|
13
|
-
AccordionContent,
|
|
14
|
-
AccordionItem,
|
|
15
|
-
AccordionTrigger,
|
|
16
|
-
} from '~/ui/accordion'
|
|
17
11
|
import { AccessRequestSection } from '../components/AccessRequestSection'
|
|
18
12
|
import { AccessPrerequisiteChain } from '../components/AccessPrerequisiteChain'
|
|
19
13
|
import { QuickJumpBar, hasQuickJumps } from '../components/QuickJumpBar'
|
|
@@ -31,8 +25,13 @@ import { formatRelativeTime } from '../../utils/formatRelativeTime'
|
|
|
31
25
|
import { TierVariantsSection } from '../components/TierVariantsSection'
|
|
32
26
|
import { SubResourcesSection } from '../components/SubResourcesSection'
|
|
33
27
|
import { getChildResources } from '../../utils/resolveHelpers'
|
|
34
|
-
import {
|
|
35
|
-
|
|
28
|
+
import {
|
|
29
|
+
ResourceDetailAccessActions,
|
|
30
|
+
ResourceDetailCardSource,
|
|
31
|
+
} from '~/modules/extensions'
|
|
32
|
+
import { CommunitySection } from './CommunitySection'
|
|
33
|
+
import type { CommunitySectionHandle } from './CommunitySection'
|
|
34
|
+
import { usePendingCount } from './usePendingCount'
|
|
36
35
|
import { displayUrl } from '~/modules/appCatalog/utils/displayUrl'
|
|
37
36
|
import { accessTabLabel } from '~/modules/appCatalog/utils/accessTabLabel'
|
|
38
37
|
import { AppDoor } from './AppDoor'
|
|
@@ -45,9 +44,28 @@ const TAB = {
|
|
|
45
44
|
access: 'access',
|
|
46
45
|
resources: 'resources',
|
|
47
46
|
documentation: 'documentation',
|
|
48
|
-
|
|
47
|
+
behind: 'behind',
|
|
49
48
|
} as const
|
|
50
49
|
|
|
50
|
+
/**
|
|
51
|
+
* One of Behind this card's top-level groups. A level above `Section`: its
|
|
52
|
+
* title is a plain heading, and a hairline separates it from the group before.
|
|
53
|
+
*/
|
|
54
|
+
function Group({
|
|
55
|
+
title,
|
|
56
|
+
children,
|
|
57
|
+
}: {
|
|
58
|
+
title: string
|
|
59
|
+
children: React.ReactNode
|
|
60
|
+
}) {
|
|
61
|
+
return (
|
|
62
|
+
<section className="behind-group">
|
|
63
|
+
<h2 className="behind-group__title">{title}</h2>
|
|
64
|
+
{children}
|
|
65
|
+
</section>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
/** A labelled block inside a tab. One register for all of them. */
|
|
52
70
|
function Section({
|
|
53
71
|
title,
|
|
@@ -123,19 +141,6 @@ export function AppDetails({
|
|
|
123
141
|
}) {
|
|
124
142
|
const [isGalleryOpen, setIsGalleryOpen] = React.useState(false)
|
|
125
143
|
const [galleryInitialIndex, setGalleryInitialIndex] = React.useState(0)
|
|
126
|
-
/**
|
|
127
|
-
* Set by the band's "Suggest a change", which has to do two things the button
|
|
128
|
-
* cannot do alone: bring the notes tab forward, and open the composer inside it.
|
|
129
|
-
* Held here because the band and the panel are siblings — neither can reach the
|
|
130
|
-
* other, and routing it through the URL would put a transient intent in a
|
|
131
|
-
* shareable link.
|
|
132
|
-
*
|
|
133
|
-
* A count, not a flag: the composer closes itself on send, so asking a second
|
|
134
|
-
* time has to be distinguishable from the first ask still being set. As a
|
|
135
|
-
* boolean the second click changed nothing and the button was dead for the rest
|
|
136
|
-
* of the card's life.
|
|
137
|
-
*/
|
|
138
|
-
const [composeRequests, setComposeRequests] = React.useState(0)
|
|
139
144
|
|
|
140
145
|
const { approvalMethods, resources: allResources } = useAppCatalogContext()
|
|
141
146
|
const { recordClick } = useAppClickHistory()
|
|
@@ -144,6 +149,39 @@ export function AppDetails({
|
|
|
144
149
|
const user = useUser()
|
|
145
150
|
const isAdmin = user?.isAdmin ?? false
|
|
146
151
|
const { state: filterState } = useAppCatalogFilters()
|
|
152
|
+
// Agrees with the Pending badges in the feed: composed by the same rules.
|
|
153
|
+
const behindCount = usePendingCount(app.slug, app.claims)
|
|
154
|
+
|
|
155
|
+
// "Last checked 10 hours ago · added 7 months ago" under the sources.
|
|
156
|
+
const lastChecked = app.freshness?.lastCheckedAt
|
|
157
|
+
const dateParts = []
|
|
158
|
+
if (lastChecked) {
|
|
159
|
+
const suffix = app.freshness?.isStale ? ' (may be out of date)' : ''
|
|
160
|
+
dateParts.push(`Last checked ${formatRelativeTime(lastChecked)}${suffix}`)
|
|
161
|
+
}
|
|
162
|
+
if (app.createdAt) {
|
|
163
|
+
const addedLabel = dateParts.length > 0 ? 'added' : 'Added'
|
|
164
|
+
dateParts.push(`${addedLabel} ${formatRelativeTime(app.createdAt)}`)
|
|
165
|
+
}
|
|
166
|
+
const datesLine = dateParts.join(' · ')
|
|
167
|
+
const datesTitle = [
|
|
168
|
+
app.freshness?.lastContentChangeAt &&
|
|
169
|
+
`Content last changed ${app.freshness.lastContentChangeAt}`,
|
|
170
|
+
lastChecked && `Last checked ${lastChecked}`,
|
|
171
|
+
app.createdAt && `Added ${app.createdAt}`,
|
|
172
|
+
]
|
|
173
|
+
.filter(Boolean)
|
|
174
|
+
.join(' · ')
|
|
175
|
+
|
|
176
|
+
// "Share what you know" asks for the input by bumping this. A counter, not a
|
|
177
|
+
// flag, so a second press while already on the tab still re-focuses. The
|
|
178
|
+
// effect runs after the click's render has mounted the tab's panel, so the
|
|
179
|
+
// ref is attached by then; opening the tab by hand bumps nothing.
|
|
180
|
+
const knowMoreRef = React.useRef<CommunitySectionHandle>(null)
|
|
181
|
+
const [shareAsk, setShareAsk] = React.useState(0)
|
|
182
|
+
React.useEffect(() => {
|
|
183
|
+
if (shareAsk) knowMoreRef.current?.focus()
|
|
184
|
+
}, [shareAsk])
|
|
147
185
|
|
|
148
186
|
// Arriving with `?sub=` means the visitor picked one child out of a search —
|
|
149
187
|
// so the card has to open ON that child's tab. Landing on Overview and
|
|
@@ -240,6 +278,8 @@ export function AppDetails({
|
|
|
240
278
|
? (app.childrenLabel ?? 'Resources')
|
|
241
279
|
: 'Environments'
|
|
242
280
|
|
|
281
|
+
const hasDocumentationTab = (app.links?.length ?? 0) > 0 || !!app.background
|
|
282
|
+
|
|
243
283
|
const tabs: DetailTab[] = [
|
|
244
284
|
{
|
|
245
285
|
id: TAB.overview,
|
|
@@ -367,263 +407,232 @@ export function AppDetails({
|
|
|
367
407
|
} satisfies DetailTab,
|
|
368
408
|
]
|
|
369
409
|
: []),
|
|
410
|
+
...(hasDocumentationTab
|
|
411
|
+
? [
|
|
412
|
+
{
|
|
413
|
+
id: TAB.documentation,
|
|
414
|
+
label: 'Documentation',
|
|
415
|
+
render: () => (
|
|
416
|
+
<>
|
|
417
|
+
{app.links && app.links.length > 0 && (
|
|
418
|
+
<Section title="Documentation" first>
|
|
419
|
+
<div className="space-y-0.5">
|
|
420
|
+
{app.links.map((link) => (
|
|
421
|
+
<a
|
|
422
|
+
key={link.url}
|
|
423
|
+
href={link.url}
|
|
424
|
+
target="_blank"
|
|
425
|
+
rel="noopener noreferrer"
|
|
426
|
+
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate"
|
|
427
|
+
>
|
|
428
|
+
<ExternalLink className="size-3 shrink-0" />
|
|
429
|
+
{link.title || displayUrl(link.url)}
|
|
430
|
+
</a>
|
|
431
|
+
))}
|
|
432
|
+
</div>
|
|
433
|
+
</Section>
|
|
434
|
+
)}
|
|
435
|
+
{/* Background: why the app exists at all. Deliberately quiet — a
|
|
436
|
+
reader who came to get access has already passed everything they
|
|
437
|
+
need, and history should not compete with the description. */}
|
|
438
|
+
{app.background && (
|
|
439
|
+
<Section title="Background" first={!app.links?.length}>
|
|
440
|
+
<MarkdownText className="prose prose-sm text-muted-foreground max-w-none text-sm [&_p]:mt-0 [&_p]:mb-2 [&_p:last-child]:mb-0">
|
|
441
|
+
{app.background}
|
|
442
|
+
</MarkdownText>
|
|
443
|
+
</Section>
|
|
444
|
+
)}
|
|
445
|
+
</>
|
|
446
|
+
),
|
|
447
|
+
} satisfies DetailTab,
|
|
448
|
+
]
|
|
449
|
+
: []),
|
|
370
450
|
{
|
|
371
|
-
id: TAB.
|
|
372
|
-
label: '
|
|
451
|
+
id: TAB.behind,
|
|
452
|
+
label: 'Behind this card',
|
|
453
|
+
chip: behindCount ? String(behindCount) : undefined,
|
|
454
|
+
ariaLabel: behindCount
|
|
455
|
+
? `Behind this card, ${behindCount} waiting for review`
|
|
456
|
+
: undefined,
|
|
373
457
|
render: () => (
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
href={link.url}
|
|
382
|
-
target="_blank"
|
|
383
|
-
rel="noopener noreferrer"
|
|
384
|
-
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-primary truncate"
|
|
385
|
-
>
|
|
386
|
-
<ExternalLink className="size-3 shrink-0" />
|
|
387
|
-
{link.title || displayUrl(link.url)}
|
|
388
|
-
</a>
|
|
389
|
-
))}
|
|
390
|
-
</div>
|
|
391
|
-
</Section>
|
|
392
|
-
)}
|
|
393
|
-
{/* Background: why the app exists at all. Deliberately quiet — a
|
|
394
|
-
reader who came to get access has already passed everything they
|
|
395
|
-
need, and history should not compete with the description. */}
|
|
396
|
-
{app.background && (
|
|
397
|
-
<Section title="Background">
|
|
398
|
-
<MarkdownText className="prose prose-sm text-muted-foreground max-w-none text-sm [&_p]:mt-0 [&_p]:mb-2 [&_p:last-child]:mb-0">
|
|
399
|
-
{app.background}
|
|
400
|
-
</MarkdownText>
|
|
401
|
-
</Section>
|
|
402
|
-
)}
|
|
458
|
+
<div className="behind-panel">
|
|
459
|
+
<CommunitySection
|
|
460
|
+
ref={knowMoreRef}
|
|
461
|
+
appSlug={app.slug}
|
|
462
|
+
appName={app.displayName}
|
|
463
|
+
claims={app.claims}
|
|
464
|
+
/>
|
|
403
465
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
<
|
|
417
|
-
{
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
setDraftSource(null)
|
|
428
|
-
|
|
466
|
+
{(isAdmin ||
|
|
467
|
+
sourceUrls.length > 0 ||
|
|
468
|
+
app.createdAt ||
|
|
469
|
+
app.freshness?.lastCheckedAt) && (
|
|
470
|
+
<Group title="Kept up to date from">
|
|
471
|
+
{isAdmin ? (
|
|
472
|
+
<>
|
|
473
|
+
<ul className="space-y-2">
|
|
474
|
+
{displaySources.map((url, index) => {
|
|
475
|
+
const isDraft =
|
|
476
|
+
draftSource !== null && index === sourceUrls.length
|
|
477
|
+
return (
|
|
478
|
+
<li
|
|
479
|
+
key={isDraft ? 'draft' : `${index}-${url}`}
|
|
480
|
+
className="group flex items-center gap-2 text-xs"
|
|
481
|
+
>
|
|
482
|
+
<span className="text-muted-foreground shrink-0 tabular-nums">
|
|
483
|
+
{index + 1}.
|
|
484
|
+
</span>
|
|
485
|
+
<InlineEditableField
|
|
486
|
+
value={url}
|
|
487
|
+
initialEditMode={isDraft}
|
|
488
|
+
onCancel={
|
|
489
|
+
isDraft ? () => setDraftSource(null) : undefined
|
|
490
|
+
}
|
|
491
|
+
onSave={(newUrl) => {
|
|
492
|
+
if (isDraft) {
|
|
493
|
+
setDraftSource(null)
|
|
494
|
+
if (newUrl) {
|
|
495
|
+
updateApp.mutate({
|
|
496
|
+
id: app.id,
|
|
497
|
+
data: { sources: [...sourceUrls, newUrl] },
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
} else {
|
|
501
|
+
const next = [...sourceUrls]
|
|
502
|
+
next[index] = newUrl
|
|
429
503
|
updateApp.mutate({
|
|
430
504
|
id: app.id,
|
|
431
|
-
data: { sources:
|
|
505
|
+
data: { sources: next.filter(Boolean) },
|
|
432
506
|
})
|
|
433
507
|
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
508
|
+
}}
|
|
509
|
+
placeholder="https://..."
|
|
510
|
+
viewClassName="flex-1 min-w-0"
|
|
511
|
+
renderView={(val) =>
|
|
512
|
+
val ? (
|
|
513
|
+
<a
|
|
514
|
+
href={val}
|
|
515
|
+
target="_blank"
|
|
516
|
+
rel="noopener noreferrer"
|
|
517
|
+
className="hover:text-primary inline-flex items-center gap-1 truncate"
|
|
518
|
+
>
|
|
519
|
+
{displayUrl(val)}
|
|
520
|
+
<ExternalLink className="size-3 shrink-0" />
|
|
521
|
+
</a>
|
|
522
|
+
) : (
|
|
523
|
+
<span className="text-muted-foreground">—</span>
|
|
524
|
+
)
|
|
441
525
|
}
|
|
442
|
-
}}
|
|
443
|
-
placeholder="https://..."
|
|
444
|
-
viewClassName="flex-1 min-w-0"
|
|
445
|
-
renderView={(val) =>
|
|
446
|
-
val ? (
|
|
447
|
-
<a
|
|
448
|
-
href={val}
|
|
449
|
-
target="_blank"
|
|
450
|
-
rel="noopener noreferrer"
|
|
451
|
-
className="hover:text-primary inline-flex items-center gap-1 truncate"
|
|
452
|
-
>
|
|
453
|
-
{displayUrl(val)}
|
|
454
|
-
<ExternalLink className="size-3 shrink-0" />
|
|
455
|
-
</a>
|
|
456
|
-
) : (
|
|
457
|
-
<span className="text-muted-foreground">—</span>
|
|
458
|
-
)
|
|
459
|
-
}
|
|
460
|
-
/>
|
|
461
|
-
{!isDraft && pulseByUrl.has(url) && (
|
|
462
|
-
<SourcePulse
|
|
463
|
-
pulse={pulseByUrl.get(url)!}
|
|
464
|
-
className="ml-auto"
|
|
465
526
|
/>
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
) : (
|
|
503
|
-
<ul className="space-y-2">
|
|
504
|
-
{sourceUrls.map((url, index) => (
|
|
505
|
-
// `group` so hovering anywhere on the row fades that source's
|
|
506
|
-
// track in — the mark sits in a right-hand column, and row
|
|
507
|
-
// hover is what ties the two ends of a wide row together.
|
|
508
|
-
<li
|
|
509
|
-
key={index}
|
|
510
|
-
className="group flex items-center gap-2 text-xs"
|
|
511
|
-
>
|
|
512
|
-
<span className="text-muted-foreground shrink-0 tabular-nums">
|
|
513
|
-
{index + 1}.
|
|
514
|
-
</span>
|
|
515
|
-
{url ? (
|
|
516
|
-
<a
|
|
517
|
-
href={url}
|
|
518
|
-
target="_blank"
|
|
519
|
-
rel="noopener noreferrer"
|
|
520
|
-
className="hover:text-primary inline-flex min-w-0 items-center gap-1 truncate"
|
|
521
|
-
>
|
|
522
|
-
{displayUrl(url)}
|
|
523
|
-
<ExternalLink className="size-3 shrink-0" />
|
|
524
|
-
</a>
|
|
525
|
-
) : (
|
|
526
|
-
<span className="text-muted-foreground">—</span>
|
|
527
|
-
)}
|
|
528
|
-
{pulseByUrl.has(url) && (
|
|
529
|
-
<SourcePulse
|
|
530
|
-
pulse={pulseByUrl.get(url)!}
|
|
531
|
-
className="ml-auto"
|
|
532
|
-
/>
|
|
533
|
-
)}
|
|
534
|
-
</li>
|
|
535
|
-
))}
|
|
536
|
-
</ul>
|
|
537
|
-
)}
|
|
538
|
-
</Section>
|
|
539
|
-
|
|
540
|
-
{/* These used to sit unlabelled in a 64px gap between two sections,
|
|
541
|
-
which is what made that gap look like a mistake. Labelled, in
|
|
542
|
-
their own block. "Dates" rather than "Added / updated" on purpose:
|
|
543
|
-
a label that repeats the first word of every line below it adds
|
|
544
|
-
nothing, and it made "Added"/"Updated" ambiguous to anything
|
|
545
|
-
matching on text — including this card's own tests. */}
|
|
546
|
-
{(app.createdAt || app.freshness?.lastCheckedAt) && (
|
|
547
|
-
<Section title="Dates">
|
|
548
|
-
<div className="flex flex-wrap gap-x-5 gap-y-1">
|
|
549
|
-
{app.createdAt && (
|
|
550
|
-
<span
|
|
551
|
-
className="inline-flex items-center gap-1 text-xs text-muted-foreground"
|
|
552
|
-
title={app.createdAt}
|
|
553
|
-
>
|
|
554
|
-
<CalendarPlus className="size-3" />
|
|
555
|
-
Added {formatRelativeTime(app.createdAt)}
|
|
556
|
-
</span>
|
|
557
|
-
)}
|
|
558
|
-
{app.freshness?.lastCheckedAt && (
|
|
559
|
-
<span
|
|
560
|
-
className="inline-flex items-center gap-1 text-xs text-muted-foreground"
|
|
561
|
-
title={
|
|
562
|
-
app.freshness.lastContentChangeAt
|
|
563
|
-
? `Content last changed ${app.freshness.lastContentChangeAt} · last checked ${app.freshness.lastCheckedAt}`
|
|
564
|
-
: `Last checked ${app.freshness.lastCheckedAt}`
|
|
565
|
-
}
|
|
527
|
+
{!isDraft && pulseByUrl.has(url) && (
|
|
528
|
+
<SourcePulse
|
|
529
|
+
pulse={pulseByUrl.get(url)!}
|
|
530
|
+
className="ml-auto"
|
|
531
|
+
/>
|
|
532
|
+
)}
|
|
533
|
+
{!isDraft && (
|
|
534
|
+
<Button
|
|
535
|
+
type="button"
|
|
536
|
+
variant="ghost"
|
|
537
|
+
size="icon-sm"
|
|
538
|
+
aria-label="Remove source"
|
|
539
|
+
className="shrink-0 text-muted-foreground hover:text-destructive"
|
|
540
|
+
onClick={() => {
|
|
541
|
+
const next = sourceUrls.filter(
|
|
542
|
+
(_, i) => i !== index,
|
|
543
|
+
)
|
|
544
|
+
updateApp.mutate({
|
|
545
|
+
id: app.id,
|
|
546
|
+
data: { sources: next },
|
|
547
|
+
})
|
|
548
|
+
}}
|
|
549
|
+
>
|
|
550
|
+
<Trash2 className="size-3.5" />
|
|
551
|
+
</Button>
|
|
552
|
+
)}
|
|
553
|
+
</li>
|
|
554
|
+
)
|
|
555
|
+
})}
|
|
556
|
+
</ul>
|
|
557
|
+
<Button
|
|
558
|
+
type="button"
|
|
559
|
+
variant="ghost"
|
|
560
|
+
size="sm"
|
|
561
|
+
className="mt-2 gap-1 text-muted-foreground"
|
|
562
|
+
onClick={() => setDraftSource('')}
|
|
566
563
|
>
|
|
567
|
-
<
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
564
|
+
<Plus className="size-3.5" />
|
|
565
|
+
Add source
|
|
566
|
+
</Button>
|
|
567
|
+
</>
|
|
568
|
+
) : (
|
|
569
|
+
<ul className="space-y-2">
|
|
570
|
+
{sourceUrls.map((url, index) => (
|
|
571
|
+
// `group` so hovering anywhere on the row fades that source's
|
|
572
|
+
// track in — the mark sits in a right-hand column, and row
|
|
573
|
+
// hover is what ties the two ends of a wide row together.
|
|
574
|
+
<li
|
|
575
|
+
key={index}
|
|
576
|
+
className="group flex items-center gap-2 text-xs"
|
|
577
|
+
>
|
|
578
|
+
<span className="text-muted-foreground shrink-0 tabular-nums">
|
|
579
|
+
{index + 1}.
|
|
580
580
|
</span>
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
581
|
+
{url ? (
|
|
582
|
+
<a
|
|
583
|
+
href={url}
|
|
584
|
+
target="_blank"
|
|
585
|
+
rel="noopener noreferrer"
|
|
586
|
+
className="hover:text-primary inline-flex min-w-0 items-center gap-1 truncate"
|
|
587
|
+
>
|
|
588
|
+
{displayUrl(url)}
|
|
589
|
+
<ExternalLink className="size-3 shrink-0" />
|
|
590
|
+
</a>
|
|
591
|
+
) : (
|
|
592
|
+
<span className="text-muted-foreground">—</span>
|
|
593
|
+
)}
|
|
594
|
+
{pulseByUrl.has(url) && (
|
|
595
|
+
<SourcePulse
|
|
596
|
+
pulse={pulseByUrl.get(url)!}
|
|
597
|
+
className="ml-auto"
|
|
598
|
+
/>
|
|
599
|
+
)}
|
|
600
|
+
</li>
|
|
601
|
+
))}
|
|
602
|
+
</ul>
|
|
603
|
+
)}
|
|
604
|
+
{datesLine && (
|
|
605
|
+
<p className="behind-dates" title={datesTitle}>
|
|
606
|
+
{datesLine}
|
|
607
|
+
</p>
|
|
608
|
+
)}
|
|
609
|
+
</Group>
|
|
586
610
|
)}
|
|
587
611
|
|
|
588
|
-
{/* Technical information: AI-facing fields, de-emphasized/collapsed */}
|
|
589
612
|
{(app.aiPrompt || app.aiMemory) && (
|
|
590
|
-
<
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
</p>
|
|
605
|
-
</div>
|
|
606
|
-
)}
|
|
607
|
-
{app.aiMemory && (
|
|
608
|
-
<div>
|
|
609
|
-
<h4 className="section-label mb-1">AI Memory</h4>
|
|
610
|
-
<p className="text-xs whitespace-pre-wrap">
|
|
611
|
-
{app.aiMemory}
|
|
612
|
-
</p>
|
|
613
|
-
</div>
|
|
614
|
-
)}
|
|
615
|
-
</AccordionContent>
|
|
616
|
-
</AccordionItem>
|
|
617
|
-
</Accordion>
|
|
613
|
+
<Group title="How the AI maintains this card">
|
|
614
|
+
{app.aiPrompt && (
|
|
615
|
+
<>
|
|
616
|
+
<p className="behind-ai__label">Prompt</p>
|
|
617
|
+
<p className="behind-ai">{app.aiPrompt}</p>
|
|
618
|
+
</>
|
|
619
|
+
)}
|
|
620
|
+
{app.aiMemory && (
|
|
621
|
+
<>
|
|
622
|
+
<p className="behind-ai__label">Memory</p>
|
|
623
|
+
<p className="behind-ai">{app.aiMemory}</p>
|
|
624
|
+
</>
|
|
625
|
+
)}
|
|
626
|
+
</Group>
|
|
618
627
|
)}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
628
|
+
|
|
629
|
+
<div className="behind-card-source">
|
|
630
|
+
<ResourceDetailCardSource
|
|
631
|
+
appSlug={app.slug}
|
|
632
|
+
appName={app.displayName}
|
|
633
|
+
/>
|
|
634
|
+
</div>
|
|
635
|
+
</div>
|
|
627
636
|
),
|
|
628
637
|
},
|
|
629
638
|
]
|
|
@@ -655,30 +664,12 @@ export function AppDetails({
|
|
|
655
664
|
: 'Deprecated'}
|
|
656
665
|
</Badge>
|
|
657
666
|
)}
|
|
658
|
-
{/*
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
card to find. Up here it is present on every tab, which is the whole
|
|
663
|
-
reason the band does not scroll away.
|
|
664
|
-
`mr-10` keeps it clear of the dialog's own close button at top-right. */}
|
|
665
|
-
{/* Plugin actions sit beside "Suggest a change", sharing its reasoning:
|
|
666
|
-
the band is mounted on every tab and never scrolls away. A slot in a
|
|
667
|
-
tab panel would be invisible until a second click, and one inside
|
|
668
|
-
SubResourceDetailPanel's `hasAnyAccess` gate would vanish for exactly
|
|
669
|
-
the resources that need help most.
|
|
670
|
-
Renders nothing when no plugin contributes, so the band keeps its
|
|
671
|
-
current shape in the open-source build. */}
|
|
667
|
+
{/* Plugin actions live in the band because it is mounted on every tab and
|
|
668
|
+
never scrolls away; a slot in a tab panel would be invisible until a
|
|
669
|
+
second click. Renders nothing when no plugin contributes.
|
|
670
|
+
`mr-10` keeps them clear of the dialog's close button at top-right. */}
|
|
672
671
|
<div className="ml-auto mr-10 flex items-center gap-2">
|
|
673
672
|
<ResourceDetailAccessActions {...accessActionsCtx} />
|
|
674
|
-
<Button
|
|
675
|
-
type="button"
|
|
676
|
-
size="sm"
|
|
677
|
-
variant="outline"
|
|
678
|
-
onClick={() => setComposeRequests((n) => n + 1)}
|
|
679
|
-
>
|
|
680
|
-
✎ Suggest a change
|
|
681
|
-
</Button>
|
|
682
673
|
</div>
|
|
683
674
|
</div>
|
|
684
675
|
|
|
@@ -780,8 +771,20 @@ export function AppDetails({
|
|
|
780
771
|
// `?sub=` means a child was picked out of a search, so the card opens
|
|
781
772
|
// on the tab that holds it rather than making them hunt for it.
|
|
782
773
|
forcedTab={subSlug && hasResourcesTab ? TAB.resources : undefined}
|
|
783
|
-
|
|
784
|
-
|
|
774
|
+
trailing={(select) => (
|
|
775
|
+
<button
|
|
776
|
+
type="button"
|
|
777
|
+
className="detail-share"
|
|
778
|
+
aria-label="Share what you know"
|
|
779
|
+
onClick={() => {
|
|
780
|
+
select(TAB.behind)
|
|
781
|
+
setShareAsk((n) => n + 1)
|
|
782
|
+
}}
|
|
783
|
+
>
|
|
784
|
+
<span className="max-sm:sr-only">Share what you know</span>
|
|
785
|
+
<Plus className="size-4 sm:hidden" aria-hidden="true" />
|
|
786
|
+
</button>
|
|
787
|
+
)}
|
|
785
788
|
/>
|
|
786
789
|
</div>
|
|
787
790
|
|