@flamingo-stack/openframe-frontend-core 0.0.549 → 0.0.550-1809.4789.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/{chunk-3UWTT6YZ.cjs → chunk-2XV6CE6M.cjs} +31 -31
- package/dist/{chunk-3UWTT6YZ.cjs.map → chunk-2XV6CE6M.cjs.map} +1 -1
- package/dist/{chunk-ZZX4FQSW.cjs → chunk-2ZJ4BJ3U.cjs} +78 -18
- package/dist/chunk-2ZJ4BJ3U.cjs.map +1 -0
- package/dist/{chunk-TBPENWIC.cjs → chunk-5B53E3UM.cjs} +3 -3
- package/dist/{chunk-TBPENWIC.cjs.map → chunk-5B53E3UM.cjs.map} +1 -1
- package/dist/{chunk-KOBSYCES.js → chunk-5IJPWW6O.js} +2 -2
- package/dist/{chunk-JHW6JAY2.cjs → chunk-6AELOXOY.cjs} +61 -61
- package/dist/{chunk-JHW6JAY2.cjs.map → chunk-6AELOXOY.cjs.map} +1 -1
- package/dist/{chunk-SYALTJFE.cjs → chunk-7374NQNY.cjs} +50 -37
- package/dist/chunk-7374NQNY.cjs.map +1 -0
- package/dist/{chunk-JDGC7ULB.js → chunk-ABIMH2FX.js} +2 -2
- package/dist/{chunk-UGAYDMIX.js → chunk-AOXFPWVN.js} +5 -5
- package/dist/{chunk-IHLJPOBD.js → chunk-AUY2RNZM.js} +2 -2
- package/dist/{chunk-YXCG5OWQ.js → chunk-EJZOTT75.js} +4 -4
- package/dist/{chunk-J7R2TX3Z.js → chunk-ETFCZVXU.js} +2 -2
- package/dist/{chunk-3GNLB5ZE.cjs → chunk-FHPSG36S.cjs} +4 -4
- package/dist/{chunk-3GNLB5ZE.cjs.map → chunk-FHPSG36S.cjs.map} +1 -1
- package/dist/{chunk-LNFFNT7U.js → chunk-FRYJCWU4.js} +2 -2
- package/dist/{chunk-7MM4CVPX.js → chunk-FS3GRXES.js} +2 -2
- package/dist/{chunk-ZIKIK2HY.js → chunk-I3LZTRRF.js} +2 -2
- package/dist/{chunk-3E6NXFCC.cjs → chunk-J2TWUHPW.cjs} +5 -5
- package/dist/{chunk-3E6NXFCC.cjs.map → chunk-J2TWUHPW.cjs.map} +1 -1
- package/dist/{chunk-RLFIDM7O.js → chunk-JC5KBKQO.js} +2 -2
- package/dist/{chunk-3UBQOT3I.cjs → chunk-JQ3UYEYL.cjs} +14 -14
- package/dist/{chunk-3UBQOT3I.cjs.map → chunk-JQ3UYEYL.cjs.map} +1 -1
- package/dist/{chunk-PNVOJORK.cjs → chunk-MYHN4PR7.cjs} +27 -27
- package/dist/{chunk-PNVOJORK.cjs.map → chunk-MYHN4PR7.cjs.map} +1 -1
- package/dist/{chunk-G4HJJTOJ.js → chunk-O2YEPKWP.js} +6 -6
- package/dist/{chunk-JV4SAJLA.cjs → chunk-OV5W7QE2.cjs} +87 -87
- package/dist/{chunk-JV4SAJLA.cjs.map → chunk-OV5W7QE2.cjs.map} +1 -1
- package/dist/{chunk-25QVYJ2Q.cjs → chunk-PPQJKUI2.cjs} +26 -26
- package/dist/{chunk-25QVYJ2Q.cjs.map → chunk-PPQJKUI2.cjs.map} +1 -1
- package/dist/{chunk-U4TKV7QM.cjs → chunk-PVDSJKGL.cjs} +9 -9
- package/dist/{chunk-U4TKV7QM.cjs.map → chunk-PVDSJKGL.cjs.map} +1 -1
- package/dist/{chunk-NSHENOGO.js → chunk-QL2UEXPY.js} +3474 -3414
- package/dist/chunk-QL2UEXPY.js.map +1 -0
- package/dist/{chunk-J4VSR65F.js → chunk-R4TNUBO4.js} +15 -2
- package/dist/chunk-R4TNUBO4.js.map +1 -0
- package/dist/{chunk-BCM7EU7R.cjs → chunk-RIF6R54C.cjs} +11 -11
- package/dist/{chunk-BCM7EU7R.cjs.map → chunk-RIF6R54C.cjs.map} +1 -1
- package/dist/{chunk-27EVFHSR.js → chunk-TSU7MYQC.js} +4 -4
- package/dist/{chunk-AIGSKRHR.cjs → chunk-ZDRGQCKN.cjs} +7 -7
- package/dist/{chunk-AIGSKRHR.cjs.map → chunk-ZDRGQCKN.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/entity-cards/employee-entry-card.d.ts +56 -0
- package/dist/components/chat/entity-cards/employee-entry-card.d.ts.map +1 -0
- package/dist/components/chat/entity-cards/how-i-work-card.d.ts +34 -0
- package/dist/components/chat/entity-cards/how-i-work-card.d.ts.map +1 -0
- package/dist/components/chat/entity-cards/index.d.ts +4 -0
- package/dist/components/chat/entity-cards/index.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/what-i-shipped-card.d.ts +10 -22
- package/dist/components/chat/entity-cards/what-i-shipped-card.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +12 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +11 -1
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/features/status-filter-component.d.ts +10 -1
- package/dist/components/features/status-filter-component.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +142 -132
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +21 -11
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +34 -34
- package/dist/components/meeting-scheduler/index.js +3 -3
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/related-content/related-content-section.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/index.cjs +12 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +11 -1
- package/dist/index.cjs +12 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +11 -1
- package/dist/types/marketing.d.ts +1 -1
- package/dist/types/marketing.d.ts.map +1 -1
- package/dist/types/video-processing.d.ts +2 -1
- package/dist/types/video-processing.d.ts.map +1 -1
- package/dist/utils/content-ref-groups.d.ts.map +1 -1
- package/dist/utils/index.cjs +2 -1
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/entity-cards/employee-entry-card.tsx +139 -0
- package/src/components/chat/entity-cards/how-i-work-card.tsx +65 -0
- package/src/components/chat/entity-cards/index.ts +4 -0
- package/src/components/chat/entity-cards/what-i-shipped-card.tsx +15 -78
- package/src/components/features/status-filter-component.tsx +12 -2
- package/src/components/related-content/related-content-section.tsx +18 -0
- package/src/stories/EmployeeEntryCard.stories.tsx +95 -0
- package/src/stories/HowIWorkCard.stories.tsx +47 -0
- package/src/stories/StatusFilterComponent.stories.tsx +74 -0
- package/src/stories/WhatIShippedCard.stories.tsx +40 -0
- package/src/stories/__fixtures__/chat-cards.ts +40 -0
- package/src/types/marketing.ts +1 -0
- package/src/types/video-processing.ts +2 -0
- package/src/utils/content-ref-groups.ts +1 -0
- package/dist/chunk-J4VSR65F.js.map +0 -1
- package/dist/chunk-NSHENOGO.js.map +0 -1
- package/dist/chunk-SYALTJFE.cjs.map +0 -1
- package/dist/chunk-ZZX4FQSW.cjs.map +0 -1
- /package/dist/{chunk-KOBSYCES.js.map → chunk-5IJPWW6O.js.map} +0 -0
- /package/dist/{chunk-JDGC7ULB.js.map → chunk-ABIMH2FX.js.map} +0 -0
- /package/dist/{chunk-UGAYDMIX.js.map → chunk-AOXFPWVN.js.map} +0 -0
- /package/dist/{chunk-IHLJPOBD.js.map → chunk-AUY2RNZM.js.map} +0 -0
- /package/dist/{chunk-YXCG5OWQ.js.map → chunk-EJZOTT75.js.map} +0 -0
- /package/dist/{chunk-J7R2TX3Z.js.map → chunk-ETFCZVXU.js.map} +0 -0
- /package/dist/{chunk-LNFFNT7U.js.map → chunk-FRYJCWU4.js.map} +0 -0
- /package/dist/{chunk-7MM4CVPX.js.map → chunk-FS3GRXES.js.map} +0 -0
- /package/dist/{chunk-ZIKIK2HY.js.map → chunk-I3LZTRRF.js.map} +0 -0
- /package/dist/{chunk-RLFIDM7O.js.map → chunk-JC5KBKQO.js.map} +0 -0
- /package/dist/{chunk-G4HJJTOJ.js.map → chunk-O2YEPKWP.js.map} +0 -0
- /package/dist/{chunk-27EVFHSR.js.map → chunk-TSU7MYQC.js.map} +0 -0
package/package.json
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { AdminContentCard } from './admin-content-card'
|
|
3
|
+
import { getProxiedImageUrl } from '../../../utils/image-proxy-stub'
|
|
4
|
+
|
|
5
|
+
/** The fields every people-hub employee entry shares. Entity-specific bindings
|
|
6
|
+
* (`WhatIShippedCard`, `HowIWorkCard`) map their own date column to `dateLabel`
|
|
7
|
+
* and pass the rest through unchanged. */
|
|
8
|
+
export interface EmployeeEntryCardData {
|
|
9
|
+
title?: string | null
|
|
10
|
+
summary?: string | null
|
|
11
|
+
status?: string | null
|
|
12
|
+
featured_image?: string | null
|
|
13
|
+
main_video_thumbnail?: string | null
|
|
14
|
+
author?: { full_name?: string | null; avatar_url?: string | null } | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface EmployeeEntryCardProps {
|
|
18
|
+
entry: EmployeeEntryCardData
|
|
19
|
+
/** Already-formatted date shown in the meta row. Bindings own the format so
|
|
20
|
+
* the UTC-pin convention stays in `utils/format`, not in the card. */
|
|
21
|
+
dateLabel?: string | null
|
|
22
|
+
/** Extra badges rendered after the status badge (e.g. discipline / level). */
|
|
23
|
+
extraBadges?: React.ReactNode
|
|
24
|
+
/** Fallback title when the entry has none — names the entity in the empty case. */
|
|
25
|
+
untitledLabel?: string
|
|
26
|
+
/** OG fallback cover. Caller computes it (hub: `useOgPlaceholderUrl`; related
|
|
27
|
+
* rail: `extras.buildOgPlaceholderUrl`). */
|
|
28
|
+
placeholderUrl?: string | null
|
|
29
|
+
/** Owner action row (dashboard). Omit for a read-only card. */
|
|
30
|
+
actions?: React.ReactNode
|
|
31
|
+
/** When provided, the WHOLE card becomes a link (related-rail click-through).
|
|
32
|
+
* Don't combine with `actions` (nested interactive). */
|
|
33
|
+
anchorProps?: React.AnchorHTMLAttributes<HTMLAnchorElement>
|
|
34
|
+
className?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const STATUS_BADGE_CLASS: Record<string, string> = {
|
|
38
|
+
published: 'bg-ods-success-secondary text-ods-success',
|
|
39
|
+
draft: 'bg-ods-warning-secondary text-ods-warning',
|
|
40
|
+
archived: 'bg-ods-border text-ods-text-secondary',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Shared badge treatment so an entity's own badges (discipline, level) sit
|
|
44
|
+
* flush with the status badge instead of re-deriving the classes per card. */
|
|
45
|
+
export function EmployeeEntryBadge({ children }: { children: React.ReactNode }) {
|
|
46
|
+
return (
|
|
47
|
+
<span className="px-2 py-1 rounded text-xs font-medium bg-ods-card border border-ods-border text-ods-text-secondary">
|
|
48
|
+
{children}
|
|
49
|
+
</span>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* THE people-hub employee-entry card. Wraps `AdminContentCard` with the canonical
|
|
55
|
+
* mapping (cover = featured_image || main_video_thumbnail, OG placeholder
|
|
56
|
+
* fallback, title, 140-char summary, status badge, author avatar+name + a date
|
|
57
|
+
* label). Every entity that renders "an employee's entry" binds to this one
|
|
58
|
+
* engine — What I Shipped and How I Work differ only in which date column they
|
|
59
|
+
* format and which extra badges they add, so the card itself cannot drift
|
|
60
|
+
* between the dashboard and the related-content rail.
|
|
61
|
+
*/
|
|
62
|
+
export function EmployeeEntryCard({
|
|
63
|
+
entry,
|
|
64
|
+
dateLabel,
|
|
65
|
+
extraBadges,
|
|
66
|
+
untitledLabel = 'Untitled entry',
|
|
67
|
+
placeholderUrl,
|
|
68
|
+
actions,
|
|
69
|
+
anchorProps,
|
|
70
|
+
className,
|
|
71
|
+
}: EmployeeEntryCardProps) {
|
|
72
|
+
const card = (
|
|
73
|
+
<AdminContentCard
|
|
74
|
+
imageUrl={entry.featured_image || entry.main_video_thumbnail}
|
|
75
|
+
placeholderUrl={placeholderUrl}
|
|
76
|
+
title={entry.title || untitledLabel}
|
|
77
|
+
summary={(entry.summary ?? '').slice(0, 140) || 'No description'}
|
|
78
|
+
badges={
|
|
79
|
+
entry.status || extraBadges ? (
|
|
80
|
+
<>
|
|
81
|
+
{entry.status ? (
|
|
82
|
+
<span
|
|
83
|
+
className={`px-2 py-1 rounded text-xs font-medium ${
|
|
84
|
+
STATUS_BADGE_CLASS[entry.status] ?? 'bg-ods-card border border-ods-border text-ods-text-secondary'
|
|
85
|
+
}`}
|
|
86
|
+
>
|
|
87
|
+
{entry.status}
|
|
88
|
+
</span>
|
|
89
|
+
) : null}
|
|
90
|
+
{extraBadges}
|
|
91
|
+
</>
|
|
92
|
+
) : null
|
|
93
|
+
}
|
|
94
|
+
meta={
|
|
95
|
+
<>
|
|
96
|
+
<span className="flex items-center gap-2 min-w-0">
|
|
97
|
+
{entry.author?.avatar_url ? (
|
|
98
|
+
<img src={getProxiedImageUrl(entry.author.avatar_url) ?? entry.author.avatar_url} alt="" className="h-5 w-5 rounded-full object-cover shrink-0" />
|
|
99
|
+
) : null}
|
|
100
|
+
<span className="truncate">{entry.author?.full_name ?? ''}</span>
|
|
101
|
+
</span>
|
|
102
|
+
{dateLabel ? <span>{dateLabel}</span> : null}
|
|
103
|
+
</>
|
|
104
|
+
}
|
|
105
|
+
actions={actions}
|
|
106
|
+
className={className}
|
|
107
|
+
/>
|
|
108
|
+
)
|
|
109
|
+
return anchorProps ? (
|
|
110
|
+
<a {...anchorProps} className="block h-full">
|
|
111
|
+
{card}
|
|
112
|
+
</a>
|
|
113
|
+
) : (
|
|
114
|
+
card
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Loading skeleton matching EmployeeEntryCard's AdminContentCard shape (3:2 cover
|
|
119
|
+
* + title / summary / meta lines). Used by the related-content rail while a
|
|
120
|
+
* group hydrates so there's no shape jump when the real card lands. */
|
|
121
|
+
export function EmployeeEntryCardSkeleton({ className }: { className?: string }) {
|
|
122
|
+
// Same convention as BlogCardSkeleton et al.: animate-pulse on the container,
|
|
123
|
+
// `bg-ods-bg` placeholder blocks, flex-grow body with an `mt-auto` avatar+name
|
|
124
|
+
// row. Shape mirrors EmployeeEntryCard's AdminContentCard (rounded-2xl, 3:2 cover).
|
|
125
|
+
return (
|
|
126
|
+
<div className={`group bg-ods-card border border-ods-border rounded-2xl overflow-hidden h-full flex flex-col animate-pulse ${className ?? ''}`}>
|
|
127
|
+
<div className="aspect-[3/2] bg-ods-bg" />
|
|
128
|
+
<div className="p-4 flex flex-col flex-grow space-y-3">
|
|
129
|
+
<div className="h-5 w-3/4 bg-ods-bg rounded" />
|
|
130
|
+
<div className="h-3 w-full bg-ods-bg/60 rounded" />
|
|
131
|
+
<div className="h-3 w-4/5 bg-ods-bg/60 rounded" />
|
|
132
|
+
<div className="mt-auto flex items-center gap-2">
|
|
133
|
+
<div className="h-8 w-8 rounded-full bg-ods-bg" />
|
|
134
|
+
<div className="h-3 w-24 bg-ods-bg/60 rounded" />
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
EmployeeEntryCard,
|
|
4
|
+
EmployeeEntryCardSkeleton,
|
|
5
|
+
EmployeeEntryBadge,
|
|
6
|
+
type EmployeeEntryCardData,
|
|
7
|
+
} from './employee-entry-card'
|
|
8
|
+
import { formatDateUTC } from '../../../utils/format'
|
|
9
|
+
|
|
10
|
+
/** Minimal row shape the card renders. Both the hub dashboard entry and the
|
|
11
|
+
* related-content hydrated row satisfy it structurally. */
|
|
12
|
+
export interface HowIWorkCardData extends EmployeeEntryCardData {
|
|
13
|
+
session_date?: string | null
|
|
14
|
+
/** Which craft the session is about — surfaced as a badge so a reader
|
|
15
|
+
* scanning the grid can tell a sales workflow from an engineering one. */
|
|
16
|
+
discipline?: string | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface HowIWorkCardProps {
|
|
20
|
+
entry: HowIWorkCardData
|
|
21
|
+
/** OG fallback cover. Caller computes it (hub: `useOgPlaceholderUrl`; related
|
|
22
|
+
* rail: `extras.buildOgPlaceholderUrl`). */
|
|
23
|
+
placeholderUrl?: string | null
|
|
24
|
+
/** Owner action row (dashboard). Omit for a read-only card. */
|
|
25
|
+
actions?: React.ReactNode
|
|
26
|
+
/** When provided, the WHOLE card becomes a link (related-rail click-through).
|
|
27
|
+
* Don't combine with `actions` (nested interactive). */
|
|
28
|
+
anchorProps?: React.AnchorHTMLAttributes<HTMLAnchorElement>
|
|
29
|
+
className?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `ops` is an initialism, so the generic capitalize would render it "Ops" —
|
|
33
|
+
* fine — but `session_date`-style multiword disciplines need the underscore
|
|
34
|
+
* stripped too. One place, so the badge matches the filter chip exactly. */
|
|
35
|
+
function disciplineLabel(discipline: string): string {
|
|
36
|
+
return discipline.replace(/_/g, ' ').replace(/\b\w/g, (ch) => ch.toUpperCase())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* THE single "How I Work" card — a thin binding over `EmployeeEntryCard` that
|
|
41
|
+
* maps the session date to the shared meta date and adds the discipline badge.
|
|
42
|
+
* Used by BOTH the people-hub dashboard (with owner `actions`) and the
|
|
43
|
+
* related-content rail (with `anchorProps`), so the card cannot drift between
|
|
44
|
+
* the two surfaces.
|
|
45
|
+
*/
|
|
46
|
+
export function HowIWorkCard({ entry, placeholderUrl, actions, anchorProps, className }: HowIWorkCardProps) {
|
|
47
|
+
return (
|
|
48
|
+
<EmployeeEntryCard
|
|
49
|
+
entry={entry}
|
|
50
|
+
// `formatDateUTC` anchors date-only strings to UTC midnight, which is what
|
|
51
|
+
// keeps the label from shifting a day between server and client render.
|
|
52
|
+
dateLabel={entry.session_date ? formatDateUTC(entry.session_date, { fallback: '' }) || null : null}
|
|
53
|
+
extraBadges={entry.discipline ? <EmployeeEntryBadge>{disciplineLabel(entry.discipline)}</EmployeeEntryBadge> : null}
|
|
54
|
+
untitledLabel="Untitled session"
|
|
55
|
+
placeholderUrl={placeholderUrl}
|
|
56
|
+
actions={actions}
|
|
57
|
+
anchorProps={anchorProps}
|
|
58
|
+
className={className}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Loading skeleton matching HowIWorkCard's shape. Shared with every other
|
|
64
|
+
* employee-entry card so the rail's placeholder never diverges from the card. */
|
|
65
|
+
export const HowIWorkCardSkeleton = EmployeeEntryCardSkeleton
|
|
@@ -27,8 +27,12 @@ export {
|
|
|
27
27
|
type CoverImageFallback,
|
|
28
28
|
} from './use-cover-image-fallback'
|
|
29
29
|
export { AdminContentCard } from './admin-content-card'
|
|
30
|
+
export { EmployeeEntryCard, EmployeeEntryCardSkeleton, EmployeeEntryBadge } from './employee-entry-card'
|
|
31
|
+
export type { EmployeeEntryCardData, EmployeeEntryCardProps } from './employee-entry-card'
|
|
30
32
|
export { WhatIShippedCard, WhatIShippedCardSkeleton } from './what-i-shipped-card'
|
|
31
33
|
export type { WhatIShippedCardData, WhatIShippedCardProps } from './what-i-shipped-card'
|
|
34
|
+
export { HowIWorkCard, HowIWorkCardSkeleton } from './how-i-work-card'
|
|
35
|
+
export type { HowIWorkCardData, HowIWorkCardProps } from './how-i-work-card'
|
|
32
36
|
|
|
33
37
|
// Moved-into-subdir flat cards
|
|
34
38
|
export { BlockCard, type BlockCardProps } from './block-card'
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { EmployeeEntryCard, EmployeeEntryCardSkeleton, type EmployeeEntryCardData } from './employee-entry-card'
|
|
3
3
|
import { formatEntryMonthUTC } from '../../../utils/format'
|
|
4
|
-
import { getProxiedImageUrl } from '../../../utils/image-proxy-stub'
|
|
5
4
|
|
|
6
5
|
/** Minimal row shape the card renders. Both the hub dashboard entry and the
|
|
7
6
|
* related-content hydrated row satisfy it structurally. */
|
|
8
|
-
export interface WhatIShippedCardData {
|
|
9
|
-
title?: string | null
|
|
10
|
-
summary?: string | null
|
|
11
|
-
status?: string | null
|
|
12
|
-
featured_image?: string | null
|
|
13
|
-
main_video_thumbnail?: string | null
|
|
7
|
+
export interface WhatIShippedCardData extends EmployeeEntryCardData {
|
|
14
8
|
entry_month?: string | null
|
|
15
|
-
author?: { full_name?: string | null; avatar_url?: string | null } | null
|
|
16
9
|
}
|
|
17
10
|
|
|
18
11
|
export interface WhatIShippedCardProps {
|
|
@@ -28,82 +21,26 @@ export interface WhatIShippedCardProps {
|
|
|
28
21
|
className?: string
|
|
29
22
|
}
|
|
30
23
|
|
|
31
|
-
const STATUS_BADGE_CLASS: Record<string, string> = {
|
|
32
|
-
published: 'bg-ods-success-secondary text-ods-success',
|
|
33
|
-
draft: 'bg-ods-warning-secondary text-ods-warning',
|
|
34
|
-
archived: 'bg-ods-border text-ods-text-secondary',
|
|
35
|
-
}
|
|
36
|
-
|
|
37
24
|
/**
|
|
38
|
-
* THE single "What I Shipped" card
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* card is byte-identical everywhere — one component, one mapping, no drift.
|
|
25
|
+
* THE single "What I Shipped" card — a thin binding over `EmployeeEntryCard`
|
|
26
|
+
* that maps the reporting month to the shared meta date. Used by BOTH the
|
|
27
|
+
* people-hub dashboard (with owner `actions`) and the related-content rail
|
|
28
|
+
* (with `anchorProps` for click-through), so the card is byte-identical
|
|
29
|
+
* everywhere — one engine, one mapping, no drift.
|
|
44
30
|
*/
|
|
45
31
|
export function WhatIShippedCard({ entry, placeholderUrl, actions, anchorProps, className }: WhatIShippedCardProps) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
32
|
+
return (
|
|
33
|
+
<EmployeeEntryCard
|
|
34
|
+
entry={entry}
|
|
35
|
+
dateLabel={formatEntryMonthUTC(entry.entry_month, 'short')}
|
|
50
36
|
placeholderUrl={placeholderUrl}
|
|
51
|
-
title={entry.title || 'Untitled entry'}
|
|
52
|
-
summary={(entry.summary ?? '').slice(0, 140) || 'No description'}
|
|
53
|
-
badges={
|
|
54
|
-
entry.status ? (
|
|
55
|
-
<span
|
|
56
|
-
className={`px-2 py-1 rounded text-xs font-medium ${
|
|
57
|
-
STATUS_BADGE_CLASS[entry.status] ?? 'bg-ods-card border border-ods-border text-ods-text-secondary'
|
|
58
|
-
}`}
|
|
59
|
-
>
|
|
60
|
-
{entry.status}
|
|
61
|
-
</span>
|
|
62
|
-
) : null
|
|
63
|
-
}
|
|
64
|
-
meta={
|
|
65
|
-
<>
|
|
66
|
-
<span className="flex items-center gap-2 min-w-0">
|
|
67
|
-
{entry.author?.avatar_url ? (
|
|
68
|
-
<img src={getProxiedImageUrl(entry.author.avatar_url) ?? entry.author.avatar_url} alt="" className="h-5 w-5 rounded-full object-cover shrink-0" />
|
|
69
|
-
) : null}
|
|
70
|
-
<span className="truncate">{entry.author?.full_name ?? ''}</span>
|
|
71
|
-
</span>
|
|
72
|
-
{month ? <span>{month}</span> : null}
|
|
73
|
-
</>
|
|
74
|
-
}
|
|
75
37
|
actions={actions}
|
|
38
|
+
anchorProps={anchorProps}
|
|
76
39
|
className={className}
|
|
77
40
|
/>
|
|
78
41
|
)
|
|
79
|
-
return anchorProps ? (
|
|
80
|
-
<a {...anchorProps} className="block h-full">
|
|
81
|
-
{card}
|
|
82
|
-
</a>
|
|
83
|
-
) : (
|
|
84
|
-
card
|
|
85
|
-
)
|
|
86
42
|
}
|
|
87
43
|
|
|
88
|
-
/** Loading skeleton matching WhatIShippedCard's
|
|
89
|
-
*
|
|
90
|
-
|
|
91
|
-
export function WhatIShippedCardSkeleton({ className }: { className?: string }) {
|
|
92
|
-
// Same convention as BlogCardSkeleton et al.: animate-pulse on the container,
|
|
93
|
-
// `bg-ods-bg` placeholder blocks, flex-grow body with an `mt-auto` avatar+name
|
|
94
|
-
// row. Shape mirrors WhatIShippedCard's AdminContentCard (rounded-2xl, 3:2 cover).
|
|
95
|
-
return (
|
|
96
|
-
<div className={`group bg-ods-card border border-ods-border rounded-2xl overflow-hidden h-full flex flex-col animate-pulse ${className ?? ''}`}>
|
|
97
|
-
<div className="aspect-[3/2] bg-ods-bg" />
|
|
98
|
-
<div className="p-4 flex flex-col flex-grow space-y-3">
|
|
99
|
-
<div className="h-5 w-3/4 bg-ods-bg rounded" />
|
|
100
|
-
<div className="h-3 w-full bg-ods-bg/60 rounded" />
|
|
101
|
-
<div className="h-3 w-4/5 bg-ods-bg/60 rounded" />
|
|
102
|
-
<div className="mt-auto flex items-center gap-2">
|
|
103
|
-
<div className="h-8 w-8 rounded-full bg-ods-bg" />
|
|
104
|
-
<div className="h-3 w-24 bg-ods-bg/60 rounded" />
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
108
|
-
)
|
|
109
|
-
}
|
|
44
|
+
/** Loading skeleton matching WhatIShippedCard's shape. Shared with every other
|
|
45
|
+
* employee-entry card so the rail's placeholder never diverges from the card. */
|
|
46
|
+
export const WhatIShippedCardSkeleton = EmployeeEntryCardSkeleton
|
|
@@ -21,6 +21,15 @@ export interface StatusFilterComponentProps {
|
|
|
21
21
|
* dashboard expose every status while gating which ones a given viewer may
|
|
22
22
|
* actually select (e.g. non-management can only pick `published`). */
|
|
23
23
|
disabledValues?: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Row label. Defaults to `Status`, so every existing call site is unchanged.
|
|
26
|
+
*
|
|
27
|
+
* Set it to reuse this row for any other single-select facet (Discipline,
|
|
28
|
+
* Level, …) instead of hand-rolling a look-alike: a duplicated row renders
|
|
29
|
+
* markup identical to this one, and React then pairs the copy against this
|
|
30
|
+
* component during hydration and reports a mismatch on the label text.
|
|
31
|
+
*/
|
|
32
|
+
label?: string;
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
/**
|
|
@@ -35,7 +44,8 @@ export function StatusFilterComponent({
|
|
|
35
44
|
showCount = false,
|
|
36
45
|
count = 0,
|
|
37
46
|
className = '',
|
|
38
|
-
disabledValues = []
|
|
47
|
+
disabledValues = [],
|
|
48
|
+
label = 'Status'
|
|
39
49
|
}: StatusFilterComponentProps) {
|
|
40
50
|
// Filter out 'all' from options since we render it separately
|
|
41
51
|
const filteredOptions = statusOptions.filter(option => option.value !== 'all');
|
|
@@ -45,7 +55,7 @@ export function StatusFilterComponent({
|
|
|
45
55
|
<div className="flex items-center gap-2">
|
|
46
56
|
<Filter className="h-4 w-4 text-ods-accent" />
|
|
47
57
|
<span className="text-h5 text-ods-text-secondary">
|
|
48
|
-
|
|
58
|
+
{label}
|
|
49
59
|
</span>
|
|
50
60
|
</div>
|
|
51
61
|
|
|
@@ -65,6 +65,7 @@ import { decideNewTab } from '../chat/utils/decide-new-tab';
|
|
|
65
65
|
// instantiates a QueryClient, so embedders need NO QueryClientProvider.
|
|
66
66
|
import { BlogCard, BlogCardSkeleton } from '../chat/entity-cards/blog-card';
|
|
67
67
|
import { WhatIShippedCard, WhatIShippedCardSkeleton } from '../chat/entity-cards/what-i-shipped-card';
|
|
68
|
+
import { HowIWorkCard, HowIWorkCardSkeleton } from '../chat/entity-cards/how-i-work-card';
|
|
68
69
|
import { CaseStudyCard, CaseStudyCardSkeleton } from '../chat/entity-cards/case-study-card';
|
|
69
70
|
import { CustomerInterviewCard, CustomerInterviewCardSkeleton } from '../chat/entity-cards/customer-interview-card';
|
|
70
71
|
import { ProductReleaseCard, ProductReleaseCardSkeleton } from '../chat/entity-cards/product-release-card';
|
|
@@ -167,6 +168,9 @@ function renderSkeletonForType(
|
|
|
167
168
|
case 'what_i_shipped':
|
|
168
169
|
// Matches the WhatIShippedCard (AdminContentCard 3:2) shape.
|
|
169
170
|
return <WhatIShippedCardSkeleton />;
|
|
171
|
+
case 'how_i_work':
|
|
172
|
+
// Same shared employee-entry shape as What I Shipped.
|
|
173
|
+
return <HowIWorkCardSkeleton />;
|
|
170
174
|
case 'marketing_campaign':
|
|
171
175
|
return adminCampaignCard ? <adminCampaignCard.Skeleton size={legacySize} /> : null;
|
|
172
176
|
case 'roadmap_item':
|
|
@@ -296,6 +300,20 @@ function CardForType({
|
|
|
296
300
|
}
|
|
297
301
|
/>
|
|
298
302
|
);
|
|
303
|
+
case 'how_i_work':
|
|
304
|
+
// THE single How I Work card — same lib component the people-hub dashboard
|
|
305
|
+
// renders. Same `anchorProps` contract as What I Shipped above (a real
|
|
306
|
+
// href only; a fallback object would wrap the card in a dead <a>).
|
|
307
|
+
return (
|
|
308
|
+
<HowIWorkCard
|
|
309
|
+
entry={item}
|
|
310
|
+
placeholderUrl={placeholderUrl}
|
|
311
|
+
anchorProps={
|
|
312
|
+
linkProps ??
|
|
313
|
+
(href ? ({ href, ...anchorAttrs } as React.AnchorHTMLAttributes<HTMLAnchorElement>) : undefined)
|
|
314
|
+
}
|
|
315
|
+
/>
|
|
316
|
+
);
|
|
299
317
|
case 'marketing_campaign':
|
|
300
318
|
return adminCampaignCard ? <adminCampaignCard.Card campaign={item} /> : null;
|
|
301
319
|
case 'roadmap_item':
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
|
2
|
+
import { EmployeeEntryCard, EmployeeEntryBadge } from '../components/chat/entity-cards/employee-entry-card'
|
|
3
|
+
import { ChatColumnDecorator, makeAnchorProps } from './__fixtures__/chat-card-decorator'
|
|
4
|
+
import { howIWorkEntry, whatIShippedEntry } from './__fixtures__/chat-cards'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof EmployeeEntryCard> = {
|
|
7
|
+
title: 'Chat/EntityCards/EmployeeEntryCard',
|
|
8
|
+
component: EmployeeEntryCard,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'fullscreen',
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
'THE people-hub employee-entry engine. `WhatIShippedCard` and `HowIWorkCard` are thin bindings over it — they differ only in which date column they format and which extra badges they add — so the states exercised here are the states of every employee-entry card. Cover falls back featured_image → main_video_thumbnail → placeholderUrl.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
decorators: [(Story) => <ChatColumnDecorator><Story /></ChatColumnDecorator>],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default meta
|
|
22
|
+
type Story = StoryObj<typeof meta>
|
|
23
|
+
|
|
24
|
+
export const Published: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
entry: whatIShippedEntry,
|
|
27
|
+
dateLabel: 'Jul 2026',
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const Draft: Story = {
|
|
32
|
+
args: {
|
|
33
|
+
entry: { ...whatIShippedEntry, status: 'draft' },
|
|
34
|
+
dateLabel: 'Jul 2026',
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const Archived: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
entry: { ...whatIShippedEntry, status: 'archived' },
|
|
41
|
+
dateLabel: 'Jul 2026',
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const WithExtraBadges: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
entry: howIWorkEntry,
|
|
48
|
+
dateLabel: 'Aug 16, 2026',
|
|
49
|
+
extraBadges: (
|
|
50
|
+
<>
|
|
51
|
+
<EmployeeEntryBadge>Marketing</EmployeeEntryBadge>
|
|
52
|
+
<EmployeeEntryBadge>Intermediate</EmployeeEntryBadge>
|
|
53
|
+
</>
|
|
54
|
+
),
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Cover falls back to the video frame when there is no featured image. */
|
|
59
|
+
export const VideoThumbnailFallback: Story = {
|
|
60
|
+
args: {
|
|
61
|
+
entry: {
|
|
62
|
+
...whatIShippedEntry,
|
|
63
|
+
featured_image: null,
|
|
64
|
+
main_video_thumbnail:
|
|
65
|
+
'https://images.unsplash.com/photo-1461749280684-dccba630e2f6?w=1200&h=675&fit=crop',
|
|
66
|
+
},
|
|
67
|
+
dateLabel: 'Jul 2026',
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The auto-created-draft state: no title yet, no summary, no cover — the card
|
|
72
|
+
* must still read as a specific entity, not an error. */
|
|
73
|
+
export const EmptyDraft: Story = {
|
|
74
|
+
args: {
|
|
75
|
+
entry: {
|
|
76
|
+
title: null,
|
|
77
|
+
summary: null,
|
|
78
|
+
status: 'draft',
|
|
79
|
+
featured_image: null,
|
|
80
|
+
main_video_thumbnail: null,
|
|
81
|
+
author: whatIShippedEntry.author,
|
|
82
|
+
},
|
|
83
|
+
dateLabel: null,
|
|
84
|
+
untitledLabel: 'Untitled session',
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Related-rail mode: the whole card is a link (never combined with actions). */
|
|
89
|
+
export const AsAnchor: Story = {
|
|
90
|
+
args: {
|
|
91
|
+
entry: whatIShippedEntry,
|
|
92
|
+
dateLabel: 'Jul 2026',
|
|
93
|
+
anchorProps: makeAnchorProps('/what-i-shipped/billing-flow'),
|
|
94
|
+
},
|
|
95
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
|
2
|
+
import { HowIWorkCard, HowIWorkCardSkeleton } from '../components/chat/entity-cards/how-i-work-card'
|
|
3
|
+
import { ChatColumnDecorator, makeAnchorProps } from './__fixtures__/chat-card-decorator'
|
|
4
|
+
import { howIWorkEntry } from './__fixtures__/chat-cards'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof HowIWorkCard> = {
|
|
7
|
+
title: 'Chat/EntityCards/HowIWorkCard',
|
|
8
|
+
component: HowIWorkCard,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'fullscreen',
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
'Card for a `how_i_work` session — a thin binding over `EmployeeEntryCard` that maps `session_date` to the meta date and adds the discipline badge. Engine states (statuses, cover fallbacks, empty draft) are covered by the EmployeeEntryCard stories.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
decorators: [(Story) => <ChatColumnDecorator><Story /></ChatColumnDecorator>],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default meta
|
|
22
|
+
type Story = StoryObj<typeof meta>
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
entry: howIWorkEntry,
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Related-rail click-through — the whole card is one anchor. */
|
|
31
|
+
export const AsAnchor: Story = {
|
|
32
|
+
args: {
|
|
33
|
+
entry: howIWorkEntry,
|
|
34
|
+
anchorProps: makeAnchorProps('/how-i-work/' + 'competitive-research-claude'),
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** No discipline picked yet (fresh draft) — badge row shows status only. */
|
|
39
|
+
export const NoDiscipline: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
entry: { ...howIWorkEntry, discipline: null, status: 'draft' },
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const Skeleton: StoryObj = {
|
|
46
|
+
render: () => <HowIWorkCardSkeleton />,
|
|
47
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import { StatusFilterComponent } from '../components/features/status-filter-component'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof StatusFilterComponent> = {
|
|
6
|
+
title: 'Features/StatusFilterComponent',
|
|
7
|
+
component: StatusFilterComponent,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'padded',
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
'THE single-select facet filter row. Renders its own "All" button and drops any option valued `all`, so callers pass real options only. The `label` prop lets one component serve every facet axis (Status, Discipline, Level) — hand-rolling a look-alike row produces markup identical to this one, and React then pairs the copy against this component during hydration and reports a text mismatch (the bug that motivated the prop).',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default meta
|
|
20
|
+
type Story = StoryObj<typeof meta>
|
|
21
|
+
|
|
22
|
+
/** Default label. Interactive — pills toggle the selection. */
|
|
23
|
+
export const Status: Story = {
|
|
24
|
+
render: function StatusStory() {
|
|
25
|
+
const [status, setStatus] = useState('all')
|
|
26
|
+
return (
|
|
27
|
+
<StatusFilterComponent
|
|
28
|
+
selectedStatus={status}
|
|
29
|
+
onStatusChange={setStatus}
|
|
30
|
+
statusOptions={[
|
|
31
|
+
{ value: 'draft', label: 'Draft (1)' },
|
|
32
|
+
{ value: 'published', label: 'Published (3)' },
|
|
33
|
+
{ value: 'archived', label: 'Archived (1)' },
|
|
34
|
+
]}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Relabeled: the same row serving a data-derived Discipline facet, counts in
|
|
41
|
+
* the labels — how the people-hub How I Work dashboard uses it. */
|
|
42
|
+
export const RelabeledFacet: Story = {
|
|
43
|
+
render: function DisciplineStory() {
|
|
44
|
+
const [discipline, setDiscipline] = useState('all')
|
|
45
|
+
return (
|
|
46
|
+
<StatusFilterComponent
|
|
47
|
+
label="Discipline"
|
|
48
|
+
selectedStatus={discipline}
|
|
49
|
+
onStatusChange={setDiscipline}
|
|
50
|
+
statusOptions={[
|
|
51
|
+
{ value: 'engineering', label: 'Engineering (4)' },
|
|
52
|
+
{ value: 'marketing', label: 'Marketing (2)' },
|
|
53
|
+
{ value: 'sales', label: 'Sales (0)' },
|
|
54
|
+
]}
|
|
55
|
+
/>
|
|
56
|
+
)
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Viewer may see every option but select only one (people-hub "Everyone" view
|
|
61
|
+
* for non-management: pinned to Published, the rest visible but disabled). */
|
|
62
|
+
export const DisabledValues: Story = {
|
|
63
|
+
args: {
|
|
64
|
+
label: 'Status',
|
|
65
|
+
selectedStatus: 'published',
|
|
66
|
+
onStatusChange: () => {},
|
|
67
|
+
statusOptions: [
|
|
68
|
+
{ value: 'draft', label: 'Draft' },
|
|
69
|
+
{ value: 'published', label: 'Published' },
|
|
70
|
+
{ value: 'archived', label: 'Archived' },
|
|
71
|
+
],
|
|
72
|
+
disabledValues: ['all', 'draft', 'archived'],
|
|
73
|
+
},
|
|
74
|
+
}
|