@igstack/app-catalog-frontend-core 6.8.4 → 6.10.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__/modules/appCatalog/ui/detail/CommunitySection.feed.test.d.ts +0 -0
  2. package/dist/esm/__tests__/modules/appCatalog/ui/detail/CommunitySection.help.test.d.ts +0 -0
  3. package/dist/esm/__tests__/modules/appCatalog/ui/detail/CommunitySection.input.test.d.ts +0 -0
  4. package/dist/esm/__tests__/modules/appCatalog/ui/detail/composeComments.test.d.ts +1 -0
  5. package/dist/esm/__tests__/ui/detail/CommunityInput.test.d.ts +1 -0
  6. package/dist/esm/index.d.ts +1 -1
  7. package/dist/esm/modules/appCatalog/hooks/useUpdateApp.d.ts +8 -0
  8. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js +22 -12
  9. package/dist/esm/modules/appCatalog/ui/components/SubResourcesSection.js.map +1 -1
  10. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js +254 -232
  11. package/dist/esm/modules/appCatalog/ui/detail/AppDetails.js.map +1 -1
  12. package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.d.ts +17 -0
  13. package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js +187 -0
  14. package/dist/esm/modules/appCatalog/ui/detail/CommentsFeed.js.map +1 -0
  15. package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.d.ts +9 -0
  16. package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js +91 -0
  17. package/dist/esm/modules/appCatalog/ui/detail/CommentsHelp.js.map +1 -0
  18. package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.d.ts +17 -0
  19. package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js +328 -0
  20. package/dist/esm/modules/appCatalog/ui/detail/CommunityInput.js.map +1 -0
  21. package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.d.ts +15 -0
  22. package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js +26 -0
  23. package/dist/esm/modules/appCatalog/ui/detail/CommunitySection.js.map +1 -0
  24. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.d.ts +8 -19
  25. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js +49 -37
  26. package/dist/esm/modules/appCatalog/ui/detail/DetailTabs.js.map +1 -1
  27. package/dist/esm/modules/appCatalog/ui/detail/communityDraft.d.ts +30 -0
  28. package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js +50 -0
  29. package/dist/esm/modules/appCatalog/ui/detail/communityDraft.js.map +1 -0
  30. package/dist/esm/modules/appCatalog/ui/detail/communityError.d.ts +6 -0
  31. package/dist/esm/modules/appCatalog/ui/detail/communityError.js +8 -0
  32. package/dist/esm/modules/appCatalog/ui/detail/communityError.js.map +1 -0
  33. package/dist/esm/modules/appCatalog/ui/detail/composeComments.d.ts +27 -0
  34. package/dist/esm/modules/appCatalog/ui/detail/composeComments.js +66 -0
  35. package/dist/esm/modules/appCatalog/ui/detail/composeComments.js.map +1 -0
  36. package/dist/esm/modules/appCatalog/ui/detail/usePendingCount.d.ts +11 -0
  37. package/dist/esm/modules/appCatalog/ui/detail/usePendingCount.js +15 -0
  38. package/dist/esm/modules/appCatalog/ui/detail/usePendingCount.js.map +1 -0
  39. package/dist/esm/modules/extensions/features/resourceDetail.d.ts +54 -1
  40. package/dist/esm/modules/extensions/features/resourceDetail.js +4 -0
  41. package/dist/esm/modules/extensions/features/resourceDetail.js.map +1 -1
  42. package/dist/esm/modules/extensions/index.d.ts +20 -4
  43. package/dist/esm/modules/extensions/index.js +10 -4
  44. package/dist/esm/modules/extensions/index.js.map +1 -1
  45. package/dist/esm/modules/extensions/types.d.ts +1 -0
  46. package/dist/index.css +525 -49
  47. package/package.json +4 -4
  48. package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.appSwitch.test.tsx +5 -2
  49. package/src/__tests__/modules/appCatalog/ui/catalog/AppDetailPanel.enter.test.tsx +5 -2
  50. package/src/__tests__/modules/appCatalog/ui/components/SubResourcesSection.adminColumn.test.tsx +41 -4
  51. package/src/__tests__/modules/appCatalog/ui/components/SubResourcesSection.test.tsx +34 -0
  52. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.background.test.tsx +9 -10
  53. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.behindThisCard.test.tsx +402 -0
  54. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.doorAndTabs.test.tsx +8 -4
  55. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.escape.test.tsx +5 -2
  56. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.knowMore.test.tsx +175 -0
  57. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.sectionOrder.test.tsx +5 -2
  58. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.subResourceTab.test.tsx +5 -2
  59. package/src/__tests__/modules/appCatalog/ui/detail/AppDetails.tags.test.tsx +5 -2
  60. package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.feed.test.tsx +281 -0
  61. package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.help.test.tsx +179 -0
  62. package/src/__tests__/modules/appCatalog/ui/detail/CommunitySection.input.test.tsx +423 -0
  63. package/src/__tests__/modules/appCatalog/ui/detail/composeComments.test.ts +170 -0
  64. package/src/__tests__/ui/detail/CommunityInput.test.tsx +39 -0
  65. package/src/index.css +525 -49
  66. package/src/index.tsx +2 -0
  67. package/src/modules/appCatalog/ui/components/SubResourcesSection.tsx +60 -36
  68. package/src/modules/appCatalog/ui/detail/AppDetails.tsx +310 -295
  69. package/src/modules/appCatalog/ui/detail/CommentsFeed.tsx +254 -0
  70. package/src/modules/appCatalog/ui/detail/CommentsHelp.tsx +102 -0
  71. package/src/modules/appCatalog/ui/detail/CommunityInput.tsx +419 -0
  72. package/src/modules/appCatalog/ui/detail/CommunitySection.tsx +41 -0
  73. package/src/modules/appCatalog/ui/detail/DetailTabs.tsx +77 -71
  74. package/src/modules/appCatalog/ui/detail/communityDraft.ts +80 -0
  75. package/src/modules/appCatalog/ui/detail/communityError.ts +10 -0
  76. package/src/modules/appCatalog/ui/detail/composeComments.ts +103 -0
  77. package/src/modules/appCatalog/ui/detail/usePendingCount.ts +26 -0
  78. package/src/modules/extensions/features/resourceDetail.ts +45 -1
  79. package/src/modules/extensions/index.ts +39 -2
  80. package/src/modules/extensions/types.ts +1 -0
  81. package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.d.ts +0 -13
  82. package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js +0 -433
  83. package/dist/esm/modules/appCatalog/ui/detail/FeedbackSection.js.map +0 -1
  84. package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.emptyState.test.tsx +0 -181
  85. package/src/__tests__/modules/appCatalog/ui/detail/FeedbackSection.reviewStatus.test.tsx +0 -131
  86. package/src/modules/appCatalog/ui/detail/FeedbackSection.tsx +0 -605
  87. /package/dist/esm/__tests__/modules/appCatalog/ui/detail/{FeedbackSection.emptyState.test.d.ts → AppDetails.behindThisCard.test.d.ts} +0 -0
  88. /package/dist/esm/__tests__/modules/appCatalog/ui/detail/{FeedbackSection.reviewStatus.test.d.ts → AppDetails.knowMore.test.d.ts} +0 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Where a half-written note lives while it is not sent.
3
+ *
4
+ * The input sits inside a tab panel, and a panel unmounts while its tab is not
5
+ * the active one — so component state alone loses whatever someone typed or
6
+ * attached the moment they look at another tab. Losing someone's words is the
7
+ * one failure this feature cannot afford: reporting has to cost almost nothing,
8
+ * and "I typed it and it vanished" is how a person learns not to bother.
9
+ *
10
+ * Session rather than local storage: a draft is in-flight work, not a
11
+ * preference. Keyed by slug so two entries do not share one draft. Storage can
12
+ * throw outright in a locked-down context; a lost draft is not worth taking the
13
+ * card down over, so every access swallows that.
14
+ */
15
+
16
+ /** One already-uploaded image waiting to be sent with the words. */
17
+ export interface PendingImage {
18
+ id: string
19
+ name: string
20
+ }
21
+
22
+ const draftKey = (slug: string) => `ac.feedback.draft.${slug}`
23
+ const imagesKey = (slug: string) => `ac.feedback.images.${slug}`
24
+
25
+ export function readDraft(slug: string): string {
26
+ try {
27
+ return sessionStorage.getItem(draftKey(slug)) ?? ''
28
+ } catch {
29
+ return ''
30
+ }
31
+ }
32
+
33
+ export function writeDraft(slug: string, value: string): void {
34
+ try {
35
+ if (value) sessionStorage.setItem(draftKey(slug), value)
36
+ else sessionStorage.removeItem(draftKey(slug))
37
+ } catch {
38
+ /* see the module comment */
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Images upload the moment they are picked, so the draft holds ids rather than
44
+ * files. Dropping an id here does NOT delete the image; the server sweeps
45
+ * whatever is never sent, which keeps "remove" instant.
46
+ */
47
+ export function readAttachments(slug: string): PendingImage[] {
48
+ try {
49
+ const raw = sessionStorage.getItem(imagesKey(slug))
50
+ if (!raw) return []
51
+ const parsed: unknown = JSON.parse(raw)
52
+ if (!Array.isArray(parsed)) return []
53
+ return parsed.filter(
54
+ (entry): entry is PendingImage =>
55
+ typeof entry === 'object' &&
56
+ entry !== null &&
57
+ typeof (entry as PendingImage).id === 'string' &&
58
+ typeof (entry as PendingImage).name === 'string',
59
+ )
60
+ } catch {
61
+ return []
62
+ }
63
+ }
64
+
65
+ export function writeAttachments(slug: string, images: PendingImage[]): void {
66
+ try {
67
+ if (images.length > 0) {
68
+ sessionStorage.setItem(imagesKey(slug), JSON.stringify(images))
69
+ } else {
70
+ sessionStorage.removeItem(imagesKey(slug))
71
+ }
72
+ } catch {
73
+ /* see the module comment */
74
+ }
75
+ }
76
+
77
+ /** The stored image, at whatever size the browser lays it out. */
78
+ export function attachmentUrl(id: string): string {
79
+ return `/api/feedback-attachments/${id}`
80
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The server's reason when it gave a readable one ("Up to 3 images"), else a
3
+ * generic line. Validation failures arrive as serialized issue lists, which
4
+ * are not for a reader.
5
+ */
6
+ export function formatCommunityError(error: unknown): string {
7
+ const reason =
8
+ error instanceof Error ? error.message.trim().replace(/\.$/, '') : ''
9
+ return reason && !/^[[{]/.test(reason) ? reason : 'Something went wrong'
10
+ }
@@ -0,0 +1,103 @@
1
+ import type {
2
+ Claim,
3
+ FeedbackView as Note,
4
+ } from '@igstack/app-catalog-backend-core'
5
+
6
+ /** `pending` = still being checked; `used` = the card now relies on it. */
7
+ export type CommentBadge = 'pending' | 'used' | null
8
+
9
+ /** One row of the Comments feed. */
10
+ export interface CommentEntry {
11
+ key: string
12
+ /** Null when a claim names no origin at all. */
13
+ who: string | null
14
+ /** Raw ISO date or instant, for the `title`. */
15
+ at: string
16
+ /** Null for a note sent without words; the feed says what it carried instead. */
17
+ text: string | null
18
+ badge: CommentBadge
19
+ /** The visitor's row, when the entry is one — what Edit, Dismiss and screenshots hang off. */
20
+ note?: Note
21
+ }
22
+
23
+ const FEEDBACK_ORIGIN = 'feedback:'
24
+
25
+ function noteBadge(note: Note, linked: Claim[]): CommentBadge {
26
+ if (note.status === null) return 'pending'
27
+ if (linked.some((c) => c.status === 'review')) return 'pending'
28
+ if (note.status === 'acknowledged') return null
29
+ if (linked.some((c) => c.status === 'active')) return 'used'
30
+ return null
31
+ }
32
+
33
+ /** Who said a claim that has no note in the list. A URL reads as its host. */
34
+ function claimWho(origin: string | undefined): string | null {
35
+ if (!origin) return null
36
+ if (origin.startsWith(FEEDBACK_ORIGIN)) return 'A visitor'
37
+ if (/^https?:\/\//.test(origin)) {
38
+ try {
39
+ return new URL(origin).host
40
+ } catch {
41
+ return origin
42
+ }
43
+ }
44
+ return origin
45
+ }
46
+
47
+ const time = (at: string) => Date.parse(at) || 0
48
+
49
+ /**
50
+ * Everything said about an entry, one entry per visitor note, newest first.
51
+ *
52
+ * A claim whose origin is `feedback:<id>` belongs to that note: it sets the
53
+ * note's badge and is not shown again. A claim with no note in the list (its
54
+ * row is gone, or it came from a person, team or URL) stands on its own, in
55
+ * its own words. Archived claims are not shown.
56
+ */
57
+ export function composeComments(
58
+ notes: Note[],
59
+ claims: Claim[],
60
+ ): CommentEntry[] {
61
+ const ids = new Set(notes.map((n) => n.id))
62
+ const byNote = new Map<string, Claim[]>()
63
+ const standalone: Claim[] = []
64
+ for (const claim of claims) {
65
+ if (claim.status === 'archived') continue
66
+ const id = claim.origin?.startsWith(FEEDBACK_ORIGIN)
67
+ ? claim.origin.slice(FEEDBACK_ORIGIN.length)
68
+ : null
69
+ if (id !== null && ids.has(id)) {
70
+ byNote.set(id, [...(byNote.get(id) ?? []), claim])
71
+ } else {
72
+ standalone.push(claim)
73
+ }
74
+ }
75
+ const entries: CommentEntry[] = [
76
+ ...notes.map(
77
+ (note): CommentEntry => ({
78
+ key: note.id,
79
+ who: note.authorName,
80
+ at: note.createdAt,
81
+ text: note.body,
82
+ badge: noteBadge(note, byNote.get(note.id) ?? []),
83
+ note,
84
+ }),
85
+ ),
86
+ ...standalone.map(
87
+ (claim, i): CommentEntry => ({
88
+ key: `claim-${i}`,
89
+ who: claimWho(claim.origin),
90
+ at: claim.addedAt,
91
+ text: claim.text,
92
+ badge: claim.status === 'active' ? 'used' : 'pending',
93
+ }),
94
+ ),
95
+ ]
96
+ return entries.sort((a, b) => time(b.at) - time(a.at))
97
+ }
98
+
99
+ /** The Behind this card count: exactly the feed's Pending badges. */
100
+ export function countPending(notes: Note[], claims: Claim[]): number {
101
+ return composeComments(notes, claims).filter((e) => e.badge === 'pending')
102
+ .length
103
+ }
@@ -0,0 +1,26 @@
1
+ import type { Claim } from '@igstack/app-catalog-backend-core'
2
+ import { useQuery } from '@tanstack/react-query'
3
+ import { useTRPC } from '~/api/infra/trpc'
4
+ import { countPending } from './composeComments'
5
+
6
+ const NO_CLAIMS: Claim[] = []
7
+
8
+ /**
9
+ * How many entries in the Comments feed say Pending: open notes, notes whose
10
+ * claim is still in review, and review claims with no note of their own.
11
+ *
12
+ * The same query the feed runs, so the tab count and the feed share one cache
13
+ * entry and one request, and are composed by the same rules. Zero while
14
+ * loading or on error: until the notes arrive, a claim cannot be matched to
15
+ * its note, and a count that is not known yet is not shown.
16
+ */
17
+ export function usePendingCount(
18
+ appSlug: string,
19
+ claims: Claim[] = NO_CLAIMS,
20
+ ): number {
21
+ const trpc = useTRPC()
22
+ const { data, error } = useQuery(
23
+ trpc.feedback.list.queryOptions({ resourceSlug: appSlug }),
24
+ )
25
+ return data || error ? countPending(data?.items ?? [], claims) : 0
26
+ }
@@ -38,7 +38,9 @@ export interface ResourceDetailSlots {
38
38
  * family has children (hundreds, in a large catalog) — keep the cell cheap and
39
39
  * defer anything expensive until it is interacted with. And the column only
40
40
  * appears when a plugin actually fills it, so a build with no plugin renders
41
- * the table exactly as before.
41
+ * the table exactly as before. A plugin that serves only some parents says so
42
+ * in `appliesTo` (see `ResourceDetailSlotScopes`), so every other parent's
43
+ * table gets no column rather than a column of empty cells.
42
44
  */
43
45
  resourceSubResourceRowActions: {
44
46
  /** The child this row is showing. */
@@ -47,6 +49,46 @@ export interface ResourceDetailSlots {
47
49
  parent: Resource
48
50
  user: PluginUser | null
49
51
  }
52
+
53
+ /**
54
+ * "What happens next" line at the foot of the "How comments work" popover,
55
+ * beside the Comments heading on Behind this card.
56
+ *
57
+ * Tells the reader what becomes of the text they send. The OSS default is
58
+ * `Every note is reviewed. Facts that check out update the card and are
59
+ * marked Used as source.` A deployment with its own review loop substitutes
60
+ * its own text.
61
+ */
62
+ resourceDetailFeedbackNote: {
63
+ appSlug: string
64
+ appName: string
65
+ }
66
+
67
+ /**
68
+ * Where this card's own definition lives, at the foot of Behind this card.
69
+ *
70
+ * A quiet link such as "View this card's source". Only the deployment knows
71
+ * where its catalog entries are kept, so the core has no default and renders
72
+ * nothing here unless a plugin contributes.
73
+ */
74
+ resourceDetailCardSource: {
75
+ appSlug: string
76
+ appName: string
77
+ }
78
+ }
79
+
80
+ /**
81
+ * What the core knows when it decides a slot's LAYOUT, before any row renders.
82
+ *
83
+ * The input to a plugin's `appliesTo` predicate for that slot. A contribution
84
+ * renders as a component, so the core cannot ask it whether it would render
85
+ * nothing; the plugin has to say up front which scopes it serves.
86
+ */
87
+ export interface ResourceDetailSlotScopes {
88
+ resourceSubResourceRowActions: {
89
+ /** The entry whose sub-resources table is being laid out. */
90
+ parent: Resource
91
+ }
50
92
  }
51
93
 
52
94
  /**
@@ -71,4 +113,6 @@ export const ResourceDetailAccessActions = mkSlot('resourceDetailAccessActions')
71
113
  export const ResourceSubResourceRowActions = mkSlot(
72
114
  'resourceSubResourceRowActions',
73
115
  )
116
+ export const ResourceDetailFeedbackNote = mkSlot('resourceDetailFeedbackNote')
117
+ export const ResourceDetailCardSource = mkSlot('resourceDetailCardSource')
74
118
  export const ResourceDetailProvider = mkWrapper('resourceDetailProvider')
@@ -5,6 +5,7 @@ import type { ReactNode } from 'react'
5
5
  // resolves it if it has mapped it, which the example app has not.
6
6
  import { useOptionalUser } from '../auth/AuthContext'
7
7
  import type {
8
+ ResourceDetailSlotScopes,
8
9
  ResourceDetailSlots,
9
10
  ResourceDetailWrappers,
10
11
  } from './features/resourceDetail'
@@ -20,9 +21,12 @@ export type { AssertDisjoint } from './types'
20
21
  export {
21
22
  ResourceDetailAccessActions,
22
23
  ResourceSubResourceRowActions,
24
+ ResourceDetailFeedbackNote,
25
+ ResourceDetailCardSource,
23
26
  ResourceDetailProvider,
24
27
  } from './features/resourceDetail'
25
28
  export type {
29
+ ResourceDetailSlotScopes,
26
30
  ResourceDetailSlots,
27
31
  ResourceDetailWrappers,
28
32
  } from './features/resourceDetail'
@@ -38,6 +42,9 @@ export type SlotSpec = ResourceDetailSlots
38
42
  /** Every wrapper slot in the core. */
39
43
  export type WrapperSpec = ResourceDetailWrappers
40
44
 
45
+ /** The layout scope of every slot whose layout depends on more than presence. */
46
+ export type SlotScopeSpec = ResourceDetailSlotScopes
47
+
41
48
  // Guard: with a second feature this becomes
42
49
  // `AssertDisjoint<ResourceDetailSlots, ServiceDeskSlots>` and fails with the
43
50
  // duplicated id spelled out in the error. With one feature it is trivially true,
@@ -63,6 +70,16 @@ export type PluginWrappers = {
63
70
  ) => ReactNode
64
71
  }
65
72
 
73
+ /**
74
+ * Which layout scopes a plugin's slot contribution serves. Absent = all of them.
75
+ *
76
+ * Only slots listed in `SlotScopeSpec` take a predicate: those are the slots
77
+ * whose layout the core decides before rendering any contribution.
78
+ */
79
+ export type PluginAppliesTo = {
80
+ [N in keyof SlotScopeSpec]?: (scope: SlotScopeSpec[N]) => boolean
81
+ }
82
+
66
83
  /**
67
84
  * One deployment-specific feature's contribution to the core's UI.
68
85
  *
@@ -79,6 +96,7 @@ export interface AcPlugin {
79
96
  name: string
80
97
  slots?: PluginSlots
81
98
  wrappers?: PluginWrappers
99
+ appliesTo?: PluginAppliesTo
82
100
  }
83
101
 
84
102
  /**
@@ -93,9 +111,28 @@ export interface AcPlugin {
93
111
  * Use it ONLY for layout. A slot must never be the only route to something the
94
112
  * core itself needs, so branching behaviour on a plugin's presence is a design
95
113
  * smell even where branching layout is not.
114
+ *
115
+ * A slot in `SlotScopeSpec` also takes the scope being laid out, and counts only
116
+ * plugins whose `appliesTo` accepts it. An `undefined` scope means there is
117
+ * nothing to decide against, so the slot reads as unfilled.
96
118
  */
97
- export function useSlotFilled(slot: keyof SlotSpec): boolean {
98
- return useExtensions().some((plugin) => Boolean(plugin.slots?.[slot]))
119
+ export function useSlotFilled<TSlot extends keyof SlotSpec>(
120
+ slot: TSlot,
121
+ ...scope: TSlot extends keyof SlotScopeSpec
122
+ ? [scope: SlotScopeSpec[TSlot] | undefined]
123
+ : []
124
+ ): boolean {
125
+ const plugins = useExtensions()
126
+ const scoped = scope.length > 0
127
+ const value = scope[0]
128
+ if (scoped && value === undefined) return false
129
+ return plugins.some((plugin) => {
130
+ if (!plugin.slots?.[slot]) return false
131
+ const applies = plugin.appliesTo?.[slot] as
132
+ | ((s: typeof value) => boolean)
133
+ | undefined
134
+ return !scoped || !applies || applies(value)
135
+ })
99
136
  }
100
137
 
101
138
  /**
@@ -34,6 +34,7 @@ export interface PluginRegistration {
34
34
  name: string
35
35
  slots?: Record<string, OpaqueRenderer | undefined>
36
36
  wrappers?: Record<string, OpaqueRenderer | undefined>
37
+ appliesTo?: Record<string, ((scope: never) => boolean) | undefined>
37
38
  }
38
39
 
39
40
  /**
@@ -1,13 +0,0 @@
1
- export declare function FeedbackSection({ appSlug,
2
- /**
3
- * Asked to open by the card's band, which is where the affordance now lives —
4
- * at the foot of this panel it was below the fold of a tab nobody opens unless
5
- * they already know what is in it.
6
- *
7
- * A count of how many times it has asked, so a second ask re-opens the composer
8
- * the first one closed on send. A boolean could not say "again".
9
- */
10
- openComposer, }: {
11
- appSlug: string;
12
- openComposer?: number;
13
- }): import("react/jsx-runtime").JSX.Element;