@flamingo-stack/openframe-frontend-core 0.0.558 → 0.0.559-snapshot.20260820073254
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-FVHFBEXH.js → chunk-2IL7SPBP.js} +2 -2
- package/dist/{chunk-UVGFICZT.js → chunk-57G6CG2E.js} +6 -6
- package/dist/{chunk-DNPQ2U7P.js → chunk-65ZXR4YJ.js} +2 -2
- package/dist/{chunk-S3NZZKNU.js → chunk-BKQWCJP5.js} +2 -2
- package/dist/{chunk-5WQPOH27.cjs → chunk-CSXEOFJ4.cjs} +5 -5
- package/dist/{chunk-5WQPOH27.cjs.map → chunk-CSXEOFJ4.cjs.map} +1 -1
- package/dist/{chunk-Z5FN5VTC.js → chunk-F3F3PVJ3.js} +2 -2
- package/dist/{chunk-R3TXAMMB.cjs → chunk-FEI4KQYW.cjs} +182 -157
- package/dist/chunk-FEI4KQYW.cjs.map +1 -0
- package/dist/{chunk-C5HQCF4R.js → chunk-GMDPQ5FQ.js} +4 -4
- package/dist/{chunk-XL2G2HHJ.cjs → chunk-GZSUDIJB.cjs} +4 -4
- package/dist/{chunk-XL2G2HHJ.cjs.map → chunk-GZSUDIJB.cjs.map} +1 -1
- package/dist/{chunk-SI64DC72.cjs → chunk-I7RGBCG6.cjs} +31 -31
- package/dist/{chunk-SI64DC72.cjs.map → chunk-I7RGBCG6.cjs.map} +1 -1
- package/dist/{chunk-YUVCH2LX.js → chunk-IGO6GNG7.js} +5 -5
- package/dist/{chunk-CWQSUYVV.cjs → chunk-MAX76H7U.cjs} +3 -3
- package/dist/{chunk-CWQSUYVV.cjs.map → chunk-MAX76H7U.cjs.map} +1 -1
- package/dist/{chunk-3TKG7SW6.cjs → chunk-N66ANBF6.cjs} +39 -39
- package/dist/{chunk-3TKG7SW6.cjs.map → chunk-N66ANBF6.cjs.map} +1 -1
- package/dist/{chunk-N3OGB543.cjs → chunk-NCU45OV4.cjs} +9 -9
- package/dist/{chunk-N3OGB543.cjs.map → chunk-NCU45OV4.cjs.map} +1 -1
- package/dist/{chunk-XDMRN3SG.cjs → chunk-NFMYNXFH.cjs} +86 -86
- package/dist/{chunk-XDMRN3SG.cjs.map → chunk-NFMYNXFH.cjs.map} +1 -1
- package/dist/{chunk-ZSZ24UY6.js → chunk-PHIRRCPY.js} +2 -2
- package/dist/{chunk-DV4CSNZD.js → chunk-QKGNSXF2.js} +2 -2
- package/dist/{chunk-VNWGXQJD.cjs → chunk-QUYIRF6U.cjs} +20 -20
- package/dist/{chunk-VNWGXQJD.cjs.map → chunk-QUYIRF6U.cjs.map} +1 -1
- package/dist/{chunk-LM6NDYGD.js → chunk-QXDEXQDI.js} +2 -2
- package/dist/{chunk-IHXUZBBV.js → chunk-RDFMBOSI.js} +1980 -1955
- package/dist/chunk-RDFMBOSI.js.map +1 -0
- package/dist/{chunk-53GNFRVQ.cjs → chunk-UR2JKFIO.cjs} +7 -7
- package/dist/{chunk-53GNFRVQ.cjs.map → chunk-UR2JKFIO.cjs.map} +1 -1
- package/dist/{chunk-GYQHUJWR.cjs → chunk-UW42K54L.cjs} +14 -14
- package/dist/{chunk-GYQHUJWR.cjs.map → chunk-UW42K54L.cjs.map} +1 -1
- package/dist/{chunk-4VLGJEKQ.js → chunk-VAAQZZHT.js} +2 -2
- package/dist/{chunk-E26KVZJM.js → chunk-WIPZ65KU.js} +2 -2
- package/dist/{chunk-FCCSHC3H.cjs → chunk-WXJZULAB.cjs} +26 -26
- package/dist/{chunk-FCCSHC3H.cjs.map → chunk-WXJZULAB.cjs.map} +1 -1
- package/dist/{chunk-R3OCP4A7.cjs → chunk-XQJPFAA7.cjs} +62 -62
- package/dist/{chunk-R3OCP4A7.cjs.map → chunk-XQJPFAA7.cjs.map} +1 -1
- package/dist/{chunk-DHSIAIFG.cjs → chunk-XRP6VHOS.cjs} +11 -11
- package/dist/{chunk-DHSIAIFG.cjs.map → chunk-XRP6VHOS.cjs.map} +1 -1
- package/dist/{chunk-5JHJX7OX.js → chunk-YNRK3N2D.js} +4 -4
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -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/board/board-column-header.d.ts.map +1 -1
- package/dist/components/features/board/types.d.ts +6 -0
- package/dist/components/features/board/types.d.ts.map +1 -1
- package/dist/components/features/index.cjs +4 -2
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +3 -1
- package/dist/components/features/video-bites-shared.cjs +7 -0
- package/dist/components/features/video-bites-shared.cjs.map +1 -1
- package/dist/components/features/video-bites-shared.d.ts +10 -0
- package/dist/components/features/video-bites-shared.d.ts.map +1 -1
- package/dist/components/features/video-bites-shared.js +7 -1
- package/dist/components/features/video-bites-shared.js.map +1 -1
- package/dist/components/features/video-bites-strip.d.ts +5 -1
- package/dist/components/features/video-bites-strip.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 +135 -131
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +15 -11
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +28 -28
- 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/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/index.cjs +4 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +3 -1
- package/dist/components/ui/ticket-status-config-row.d.ts.map +1 -1
- package/dist/components/ui/touch-friendly-tooltip.d.ts +14 -0
- package/dist/components/ui/touch-friendly-tooltip.d.ts.map +1 -0
- package/dist/index.cjs +6 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -1
- package/dist/types/video-processing.d.ts +1 -0
- package/dist/types/video-processing.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/features/__tests__/video-bites-shared.test.ts +64 -0
- package/src/components/features/board/board-column-header.tsx +14 -1
- package/src/components/features/board/types.ts +7 -0
- package/src/components/features/video-bites-shared.ts +16 -0
- package/src/components/features/video-bites-strip.tsx +7 -2
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/ticket-status-config-row.tsx +5 -55
- package/src/components/ui/touch-friendly-tooltip.tsx +71 -0
- package/src/types/video-processing.ts +1 -0
- package/dist/chunk-IHXUZBBV.js.map +0 -1
- package/dist/chunk-R3TXAMMB.cjs.map +0 -1
- /package/dist/{chunk-FVHFBEXH.js.map → chunk-2IL7SPBP.js.map} +0 -0
- /package/dist/{chunk-UVGFICZT.js.map → chunk-57G6CG2E.js.map} +0 -0
- /package/dist/{chunk-DNPQ2U7P.js.map → chunk-65ZXR4YJ.js.map} +0 -0
- /package/dist/{chunk-S3NZZKNU.js.map → chunk-BKQWCJP5.js.map} +0 -0
- /package/dist/{chunk-Z5FN5VTC.js.map → chunk-F3F3PVJ3.js.map} +0 -0
- /package/dist/{chunk-C5HQCF4R.js.map → chunk-GMDPQ5FQ.js.map} +0 -0
- /package/dist/{chunk-YUVCH2LX.js.map → chunk-IGO6GNG7.js.map} +0 -0
- /package/dist/{chunk-ZSZ24UY6.js.map → chunk-PHIRRCPY.js.map} +0 -0
- /package/dist/{chunk-DV4CSNZD.js.map → chunk-QKGNSXF2.js.map} +0 -0
- /package/dist/{chunk-LM6NDYGD.js.map → chunk-QXDEXQDI.js.map} +0 -0
- /package/dist/{chunk-4VLGJEKQ.js.map → chunk-VAAQZZHT.js.map} +0 -0
- /package/dist/{chunk-E26KVZJM.js.map → chunk-WIPZ65KU.js.map} +0 -0
- /package/dist/{chunk-5JHJX7OX.js.map → chunk-YNRK3N2D.js.map} +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
sortBitesByCreatedAtDesc,
|
|
4
|
+
sortBitesByFeaturedAtDesc,
|
|
5
|
+
} from '../video-bites-shared'
|
|
6
|
+
import type { VideoTeaser } from '../../../types/video-processing'
|
|
7
|
+
|
|
8
|
+
const bite = (overrides: Partial<VideoTeaser>): VideoTeaser => ({
|
|
9
|
+
url: `https://cdn.example/${Math.abs(JSON.stringify(overrides).length)}.mp4`,
|
|
10
|
+
...overrides,
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('sortBitesByCreatedAtDesc', () => {
|
|
14
|
+
it('orders newest created_at first', () => {
|
|
15
|
+
const a = bite({ created_at: '2026-01-01T00:00:00Z' })
|
|
16
|
+
const b = bite({ created_at: '2026-02-01T00:00:00Z' })
|
|
17
|
+
expect([a, b].sort(sortBitesByCreatedAtDesc)).toEqual([b, a])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('sorts bites without created_at last', () => {
|
|
21
|
+
const dated = bite({ created_at: '2026-01-01T00:00:00Z' })
|
|
22
|
+
const undated = bite({})
|
|
23
|
+
expect([undated, dated].sort(sortBitesByCreatedAtDesc)).toEqual([dated, undated])
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('sortBitesByFeaturedAtDesc', () => {
|
|
28
|
+
it('ranks a recently-featured OLD bite above a newer un-restamped one', () => {
|
|
29
|
+
// The 2026-08-18 homepage regression: an old clip starred today must
|
|
30
|
+
// outrank clips created later but featured earlier.
|
|
31
|
+
const oldClipFeaturedToday = bite({
|
|
32
|
+
created_at: '2026-07-14T00:00:00Z',
|
|
33
|
+
featured_at: '2026-08-18T00:00:00Z',
|
|
34
|
+
})
|
|
35
|
+
const newerClipNoStamp = bite({ created_at: '2026-07-17T00:00:00Z' })
|
|
36
|
+
expect([newerClipNoStamp, oldClipFeaturedToday].sort(sortBitesByFeaturedAtDesc)).toEqual([
|
|
37
|
+
oldClipFeaturedToday,
|
|
38
|
+
newerClipNoStamp,
|
|
39
|
+
])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('ranks by featured_at only — created_at never influences featured order', () => {
|
|
43
|
+
// No date-fallback ranking: all featured bites carry the stamp (write
|
|
44
|
+
// path + 2026-08-19 backfill).
|
|
45
|
+
const staleStampNewClip = bite({
|
|
46
|
+
created_at: '2026-08-01T00:00:00Z',
|
|
47
|
+
featured_at: '2026-01-01T00:00:00Z',
|
|
48
|
+
})
|
|
49
|
+
const freshStampOldClip = bite({
|
|
50
|
+
created_at: '2026-01-01T00:00:00Z',
|
|
51
|
+
featured_at: '2026-08-01T00:00:00Z',
|
|
52
|
+
})
|
|
53
|
+
expect([staleStampNewClip, freshStampOldClip].sort(sortBitesByFeaturedAtDesc)).toEqual([
|
|
54
|
+
freshStampOldClip,
|
|
55
|
+
staleStampNewClip,
|
|
56
|
+
])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('sorts unstamped bites last (mid-deploy transient only)', () => {
|
|
60
|
+
const stamped = bite({ featured_at: '2026-01-01T00:00:00Z' })
|
|
61
|
+
const bare = bite({ created_at: '2026-08-01T00:00:00Z' })
|
|
62
|
+
expect([bare, stamped].sort(sortBitesByFeaturedAtDesc)).toEqual([stamped, bare])
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { Button } from '../../ui/button/button'
|
|
4
4
|
import { TicketStatusTag, resolveTicketStatus } from '../../ui/ticket-status-tag'
|
|
5
|
-
import {
|
|
5
|
+
import { TouchFriendlyTooltip } from '../../ui/touch-friendly-tooltip'
|
|
6
|
+
import { Arrow02LeftIcon, Arrow02RightIcon, BoxArchiveIcon, InfoCircleIcon, PlusIcon } from '../../icons-v2-generated'
|
|
6
7
|
import type { BoardColumnDef } from './types'
|
|
7
8
|
|
|
8
9
|
export interface BoardColumnHeaderProps {
|
|
@@ -60,6 +61,18 @@ export function BoardColumnHeader({
|
|
|
60
61
|
</span>
|
|
61
62
|
</div>
|
|
62
63
|
<div className="flex shrink-0 items-center gap-[var(--spacing-system-xxs)]">
|
|
64
|
+
{column.tooltip && (
|
|
65
|
+
<TouchFriendlyTooltip content={column.tooltip} side="bottom">
|
|
66
|
+
<Button
|
|
67
|
+
variant="transparent"
|
|
68
|
+
size="icon"
|
|
69
|
+
className="h-8 w-8 md:h-8 md:w-8 p-0"
|
|
70
|
+
aria-label={column.tooltip}
|
|
71
|
+
>
|
|
72
|
+
<InfoCircleIcon className="h-6 w-6 text-ods-text-secondary" />
|
|
73
|
+
</Button>
|
|
74
|
+
</TouchFriendlyTooltip>
|
|
75
|
+
)}
|
|
63
76
|
<Button
|
|
64
77
|
variant="transparent"
|
|
65
78
|
size="icon"
|
|
@@ -76,6 +76,12 @@ export interface BoardColumnDef {
|
|
|
76
76
|
statusKey?: string
|
|
77
77
|
label: string
|
|
78
78
|
color: string
|
|
79
|
+
/**
|
|
80
|
+
* What this column's status means, shown from an info icon in the header
|
|
81
|
+
* (e.g. the system-status descriptions from the status settings page). No
|
|
82
|
+
* icon is rendered when absent.
|
|
83
|
+
*/
|
|
84
|
+
tooltip?: string
|
|
79
85
|
tickets: BoardTicket[]
|
|
80
86
|
|
|
81
87
|
total?: number
|
|
@@ -117,6 +123,7 @@ export function columnFromTicketStatus(
|
|
|
117
123
|
id: status,
|
|
118
124
|
label: overrides.label ?? defaults.label,
|
|
119
125
|
color: overrides.color ?? defaults.color,
|
|
126
|
+
tooltip: overrides.tooltip,
|
|
120
127
|
tickets,
|
|
121
128
|
total: overrides.total,
|
|
122
129
|
hasMore: overrides.hasMore,
|
|
@@ -60,3 +60,19 @@ export function sortBitesByCreatedAtDesc(a: VideoTeaser, b: VideoTeaser): number
|
|
|
60
60
|
if (!b.created_at) return -1
|
|
61
61
|
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* FEATURED-recency comparator: `featured_at` DESC — the stamp every featured
|
|
66
|
+
* bite carries (written by the admin star toggle; pre-existing featured bites
|
|
67
|
+
* were backfilled 2026-08-19, so there is no date-fallback ranking). The
|
|
68
|
+
* comparator FEATURED surfaces (the cross-entity aggregation + its strips)
|
|
69
|
+
* must use — ranking those surfaces by `created_at` made featuring any older
|
|
70
|
+
* bite a silent no-op once the surface cap filled with newer bites. A missing
|
|
71
|
+
* stamp (only possible from a stale mid-deploy writer) sorts last.
|
|
72
|
+
*/
|
|
73
|
+
export function sortBitesByFeaturedAtDesc(a: VideoTeaser, b: VideoTeaser): number {
|
|
74
|
+
if (!a.featured_at && !b.featured_at) return 0
|
|
75
|
+
if (!a.featured_at) return 1
|
|
76
|
+
if (!b.featured_at) return -1
|
|
77
|
+
return new Date(b.featured_at).getTime() - new Date(a.featured_at).getTime()
|
|
78
|
+
}
|
|
@@ -88,6 +88,10 @@ export interface VideoBitesStripProps {
|
|
|
88
88
|
showChevrons?: boolean;
|
|
89
89
|
/** Section-level navigation fallback (per-bite `href`/`onNavigate` win). */
|
|
90
90
|
onBiteNavigate?: (bite: VideoBiteStripItem, index: number) => void;
|
|
91
|
+
/** Ordering comparator. Default `sortBitesByCreatedAtDesc`; FEATURED
|
|
92
|
+
* surfaces pass `sortBitesByFeaturedAtDesc` so display order matches the
|
|
93
|
+
* featured-recency ranking their server aggregation is capped by. */
|
|
94
|
+
sortComparator?: (a: VideoBiteStripItem, b: VideoBiteStripItem) => number;
|
|
91
95
|
className?: string;
|
|
92
96
|
}
|
|
93
97
|
|
|
@@ -110,12 +114,13 @@ export function VideoBitesStrip({
|
|
|
110
114
|
cardHeightMobile = 300,
|
|
111
115
|
showChevrons = true,
|
|
112
116
|
onBiteNavigate,
|
|
117
|
+
sortComparator = sortBitesByCreatedAtDesc,
|
|
113
118
|
className,
|
|
114
119
|
}: VideoBitesStripProps): React.ReactElement | null {
|
|
115
120
|
const items = useMemo(() => {
|
|
116
121
|
const filtered = filterPublished ? bites.filter(b => b.published) : [...bites];
|
|
117
|
-
return filtered.sort(
|
|
118
|
-
}, [bites, filterPublished]);
|
|
122
|
+
return filtered.sort(sortComparator);
|
|
123
|
+
}, [bites, filterPublished, sortComparator]);
|
|
119
124
|
|
|
120
125
|
// Preconnect Mux/Supabase origins once so first hover starts fast.
|
|
121
126
|
const warmup = useVideoWarmup<HTMLDivElement>({ videoUrl: items[0]?.url || null });
|
|
@@ -11,7 +11,7 @@ import { ColorPresetSelect, ColorPickerInput } from './color-preset-select'
|
|
|
11
11
|
import { Input } from './input'
|
|
12
12
|
import { type TagProps } from './tag'
|
|
13
13
|
import { TicketStatusTag } from './ticket-status-tag'
|
|
14
|
-
import {
|
|
14
|
+
import { TouchFriendlyTooltip } from './touch-friendly-tooltip'
|
|
15
15
|
|
|
16
16
|
type SystemTagVariant = Extract<TagProps['variant'], 'outline' | 'primary'>
|
|
17
17
|
|
|
@@ -37,56 +37,6 @@ export interface TicketStatusConfigRowProps {
|
|
|
37
37
|
isDragging?: boolean
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
let dismissOpenTooltip: (() => void) | null = null
|
|
41
|
-
|
|
42
|
-
function WithLeftTooltip({ content, children }: { content?: string; children: React.ReactElement }) {
|
|
43
|
-
const [open, setOpen] = React.useState(false)
|
|
44
|
-
const isTouchRef = React.useRef(false)
|
|
45
|
-
const close = React.useCallback(() => setOpen(false), [])
|
|
46
|
-
|
|
47
|
-
if (!content) return children
|
|
48
|
-
|
|
49
|
-
const toggleFromTouch = () => {
|
|
50
|
-
setOpen(prev => {
|
|
51
|
-
const next = !prev
|
|
52
|
-
if (next) {
|
|
53
|
-
if (dismissOpenTooltip && dismissOpenTooltip !== close) dismissOpenTooltip()
|
|
54
|
-
dismissOpenTooltip = close
|
|
55
|
-
} else if (dismissOpenTooltip === close) {
|
|
56
|
-
dismissOpenTooltip = null
|
|
57
|
-
}
|
|
58
|
-
return next
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
<TooltipProvider delayDuration={150}>
|
|
64
|
-
<Tooltip open={open} onOpenChange={setOpen}>
|
|
65
|
-
<TooltipTrigger
|
|
66
|
-
asChild
|
|
67
|
-
onPointerDown={e => {
|
|
68
|
-
isTouchRef.current = e.pointerType === 'touch'
|
|
69
|
-
if (!isTouchRef.current) return
|
|
70
|
-
e.preventDefault()
|
|
71
|
-
toggleFromTouch()
|
|
72
|
-
}}
|
|
73
|
-
onFocus={e => {
|
|
74
|
-
if (isTouchRef.current) e.preventDefault()
|
|
75
|
-
}}
|
|
76
|
-
onClick={e => {
|
|
77
|
-
if (isTouchRef.current) e.preventDefault()
|
|
78
|
-
}}
|
|
79
|
-
>
|
|
80
|
-
{children}
|
|
81
|
-
</TooltipTrigger>
|
|
82
|
-
<TooltipContent side="left" className="max-w-xs">
|
|
83
|
-
{content}
|
|
84
|
-
</TooltipContent>
|
|
85
|
-
</Tooltip>
|
|
86
|
-
</TooltipProvider>
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
40
|
export function TicketStatusConfigRow({
|
|
91
41
|
variant,
|
|
92
42
|
statusKey,
|
|
@@ -179,7 +129,7 @@ export function TicketStatusConfigRow({
|
|
|
179
129
|
|
|
180
130
|
<div className="flex h-11 w-12 shrink-0 items-center justify-center md:h-12">
|
|
181
131
|
{isSystem ? (
|
|
182
|
-
<
|
|
132
|
+
<TouchFriendlyTooltip content={systemTooltip}>
|
|
183
133
|
<button
|
|
184
134
|
type="button"
|
|
185
135
|
aria-label={systemTooltip ?? 'System status'}
|
|
@@ -187,9 +137,9 @@ export function TicketStatusConfigRow({
|
|
|
187
137
|
>
|
|
188
138
|
<InfoCircleIcon size={24} />
|
|
189
139
|
</button>
|
|
190
|
-
</
|
|
140
|
+
</TouchFriendlyTooltip>
|
|
191
141
|
) : (
|
|
192
|
-
<
|
|
142
|
+
<TouchFriendlyTooltip content={deleteDisabled ? deleteDisabledReason : undefined}>
|
|
193
143
|
<span className="inline-flex">
|
|
194
144
|
<Button
|
|
195
145
|
type="button"
|
|
@@ -203,7 +153,7 @@ export function TicketStatusConfigRow({
|
|
|
203
153
|
<TrashIcon className="text-ods-error"/>
|
|
204
154
|
</Button>
|
|
205
155
|
</span>
|
|
206
|
-
</
|
|
156
|
+
</TouchFriendlyTooltip>
|
|
207
157
|
)}
|
|
208
158
|
</div>
|
|
209
159
|
</div>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The one tooltip allowed open at a time, across every instance — tapping a
|
|
8
|
+
* second trigger dismisses the first, since touch has no "mouse left" to close
|
|
9
|
+
* it. Module-level on purpose: instances live in unrelated subtrees.
|
|
10
|
+
*/
|
|
11
|
+
let dismissOpenTooltip: (() => void) | null = null
|
|
12
|
+
|
|
13
|
+
export interface TouchFriendlyTooltipProps {
|
|
14
|
+
/** Tooltip text; when absent the children render bare, with no trigger wiring. */
|
|
15
|
+
content?: string
|
|
16
|
+
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
17
|
+
children: React.ReactElement
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hover tooltip that also works on touch, where Radix's hover/focus model never
|
|
22
|
+
* opens: a tap toggles it instead. Wraps its own `TooltipProvider`, so it can be
|
|
23
|
+
* dropped anywhere without a provider up the tree.
|
|
24
|
+
*/
|
|
25
|
+
export function TouchFriendlyTooltip({ content, side = 'left', children }: TouchFriendlyTooltipProps) {
|
|
26
|
+
const [open, setOpen] = React.useState(false)
|
|
27
|
+
const isTouchRef = React.useRef(false)
|
|
28
|
+
const close = React.useCallback(() => setOpen(false), [])
|
|
29
|
+
|
|
30
|
+
if (!content) return children
|
|
31
|
+
|
|
32
|
+
const toggleFromTouch = () => {
|
|
33
|
+
setOpen(prev => {
|
|
34
|
+
const next = !prev
|
|
35
|
+
if (next) {
|
|
36
|
+
if (dismissOpenTooltip && dismissOpenTooltip !== close) dismissOpenTooltip()
|
|
37
|
+
dismissOpenTooltip = close
|
|
38
|
+
} else if (dismissOpenTooltip === close) {
|
|
39
|
+
dismissOpenTooltip = null
|
|
40
|
+
}
|
|
41
|
+
return next
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<TooltipProvider delayDuration={150}>
|
|
47
|
+
<Tooltip open={open} onOpenChange={setOpen}>
|
|
48
|
+
<TooltipTrigger
|
|
49
|
+
asChild
|
|
50
|
+
onPointerDown={e => {
|
|
51
|
+
isTouchRef.current = e.pointerType === 'touch'
|
|
52
|
+
if (!isTouchRef.current) return
|
|
53
|
+
e.preventDefault()
|
|
54
|
+
toggleFromTouch()
|
|
55
|
+
}}
|
|
56
|
+
onFocus={e => {
|
|
57
|
+
if (isTouchRef.current) e.preventDefault()
|
|
58
|
+
}}
|
|
59
|
+
onClick={e => {
|
|
60
|
+
if (isTouchRef.current) e.preventDefault()
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
{children}
|
|
64
|
+
</TooltipTrigger>
|
|
65
|
+
<TooltipContent side={side} className="max-w-xs">
|
|
66
|
+
{content}
|
|
67
|
+
</TooltipContent>
|
|
68
|
+
</Tooltip>
|
|
69
|
+
</TooltipProvider>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -19,6 +19,7 @@ export interface VideoTeaser {
|
|
|
19
19
|
thumbnail_url?: string // Optional thumbnail image URL for video preview. If not provided, video player will show first frame automatically.
|
|
20
20
|
published?: boolean // Controls visibility on public preview page (default: false, admin must select)
|
|
21
21
|
featured?: boolean // Opts the bite into cross-entity featured pulls (homepage/configured surfaces). Independent of `published`.
|
|
22
|
+
featured_at?: string // ISO timestamp stamped when an admin features the bite (cleared on unfeature; all featured bites backfilled 2026-08-19). Featured surfaces rank by THIS — ranking by created_at made featuring an old bite invisible under the surface cap.
|
|
22
23
|
source?: 'manual' | 'ai_generated' // Track origin of teaser
|
|
23
24
|
created_at?: string // ISO timestamp for sorting (newer items first)
|
|
24
25
|
start_time_ms?: number // Vizard extraction: clip start offset in the source video
|