@flamingo-stack/openframe-frontend-core 0.0.501 → 0.0.502-snapshot.20260731145308
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-PDTCSZBY.cjs → chunk-2OXJ6NHI.cjs} +5 -5
- package/dist/{chunk-PDTCSZBY.cjs.map → chunk-2OXJ6NHI.cjs.map} +1 -1
- package/dist/{chunk-2CEFVI7Q.cjs → chunk-3T4SMJM5.cjs} +73 -73
- package/dist/{chunk-2CEFVI7Q.cjs.map → chunk-3T4SMJM5.cjs.map} +1 -1
- package/dist/{chunk-AQAZTEI2.js → chunk-4SR2QHMB.js} +2 -2
- package/dist/{chunk-I64ZGPA6.cjs → chunk-4UNQMYP7.cjs} +3 -3
- package/dist/{chunk-I64ZGPA6.cjs.map → chunk-4UNQMYP7.cjs.map} +1 -1
- package/dist/{chunk-LNQUMZTP.cjs → chunk-5RIHLXEP.cjs} +29 -29
- package/dist/{chunk-LNQUMZTP.cjs.map → chunk-5RIHLXEP.cjs.map} +1 -1
- package/dist/{chunk-2GI3BERF.cjs → chunk-72DF3QTA.cjs} +12 -12
- package/dist/{chunk-2GI3BERF.cjs.map → chunk-72DF3QTA.cjs.map} +1 -1
- package/dist/{chunk-UWNL733B.cjs → chunk-7L2RVINX.cjs} +32 -32
- package/dist/{chunk-UWNL733B.cjs.map → chunk-7L2RVINX.cjs.map} +1 -1
- package/dist/{chunk-2GCFMDJ7.cjs → chunk-AWDHRIL3.cjs} +73 -73
- package/dist/{chunk-2GCFMDJ7.cjs.map → chunk-AWDHRIL3.cjs.map} +1 -1
- package/dist/{chunk-G5LTKJ54.js → chunk-BZF63JXP.js} +5 -5
- package/dist/{chunk-O2XKSMIK.js → chunk-CLQEBWV7.js} +3 -3
- package/dist/{chunk-NBKOPZQA.cjs → chunk-DMXB3VE2.cjs} +610 -575
- package/dist/chunk-DMXB3VE2.cjs.map +1 -0
- package/dist/{chunk-P7IOCYGD.js → chunk-F7AD5CUU.js} +2 -2
- package/dist/{chunk-C5R2OX7A.cjs → chunk-FFXNRJ5S.cjs} +2 -2
- package/dist/{chunk-C5R2OX7A.cjs.map → chunk-FFXNRJ5S.cjs.map} +1 -1
- package/dist/{chunk-X26TVHZN.js → chunk-FTOVZ7O2.js} +6 -6
- package/dist/{chunk-JCISIQHT.js → chunk-G43CEISB.js} +4 -4
- package/dist/{chunk-FRJ2YKAJ.js → chunk-HR6F6IP4.js} +5 -5
- package/dist/{chunk-T4J7ZFMP.cjs → chunk-J4T5ZBWG.cjs} +15 -15
- package/dist/{chunk-T4J7ZFMP.cjs.map → chunk-J4T5ZBWG.cjs.map} +1 -1
- package/dist/{chunk-UNQ3BINL.js → chunk-JGXAVNZQ.js} +3304 -3269
- package/dist/chunk-JGXAVNZQ.js.map +1 -0
- package/dist/{chunk-WIGZI5TT.cjs → chunk-JIZJDAXI.cjs} +7 -7
- package/dist/{chunk-WIGZI5TT.cjs.map → chunk-JIZJDAXI.cjs.map} +1 -1
- package/dist/{chunk-7HCAAZQ7.js → chunk-OAWFYZZT.js} +2 -2
- package/dist/{chunk-POAM7WDT.cjs → chunk-RR3Z52X5.cjs} +40 -40
- package/dist/{chunk-POAM7WDT.cjs.map → chunk-RR3Z52X5.cjs.map} +1 -1
- package/dist/{chunk-VPQBQJ4S.js → chunk-SFJRMBJC.js} +2 -2
- package/dist/{chunk-EOI74NPQ.cjs → chunk-SGWPW2AO.cjs} +18 -18
- package/dist/{chunk-EOI74NPQ.cjs.map → chunk-SGWPW2AO.cjs.map} +1 -1
- package/dist/{chunk-RHDMGCIW.cjs → chunk-UCV4LFUG.cjs} +40 -40
- package/dist/{chunk-RHDMGCIW.cjs.map → chunk-UCV4LFUG.cjs.map} +1 -1
- package/dist/{chunk-6TOLZW2V.js → chunk-VCPTU4IW.js} +6 -6
- package/dist/{chunk-QTJXAVZ3.js → chunk-VO2ZONMR.js} +3 -3
- package/dist/{chunk-XOF453AB.js → chunk-VYEDTVIZ.js} +3 -3
- package/dist/{chunk-BRRHC7GH.js → chunk-XV5SXALY.js} +2 -2
- package/dist/components/case-studies/index.cjs +9 -9
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/chat/index.cjs +3 -3
- package/dist/components/chat/index.js +2 -2
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/content-loading-container.d.ts +1 -1
- package/dist/components/docs/index.cjs +6 -6
- package/dist/components/docs/index.js +5 -5
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/faq/index.js +4 -4
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- 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 +136 -136
- package/dist/components/index.js +11 -11
- package/dist/components/navigation/index.cjs +3 -3
- package/dist/components/navigation/index.js +2 -2
- package/dist/components/onboarding-guides/index.cjs +7 -7
- package/dist/components/onboarding-guides/index.js +6 -6
- package/dist/components/related-content/index.cjs +5 -5
- package/dist/components/related-content/index.js +4 -4
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/autocomplete.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-column-filter.d.ts +3 -1
- package/dist/components/ui/data-table/data-table-column-filter.d.ts.map +1 -1
- package/dist/components/ui/data-table/types.d.ts +12 -1
- package/dist/components/ui/data-table/types.d.ts.map +1 -1
- package/dist/components/ui/filter-modal.d.ts.map +1 -1
- package/dist/components/ui/floating-tooltip.d.ts +9 -4
- package/dist/components/ui/floating-tooltip.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/components/ui/service-card.d.ts.map +1 -1
- package/dist/components/ui/status-badge.d.ts +1 -1
- package/dist/components/ui/tag.d.ts +1 -1
- package/dist/components/ui/tag.d.ts.map +1 -1
- package/dist/components/ui/truncate-text.d.ts +14 -1
- package/dist/components/ui/truncate-text.d.ts.map +1 -1
- package/dist/hooks/index.cjs +2 -2
- package/dist/hooks/index.js +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/src/components/ui/autocomplete.tsx +5 -2
- package/src/components/ui/data-table/data-table-column-filter.tsx +33 -24
- package/src/components/ui/data-table/data-table-header.tsx +1 -0
- package/src/components/ui/data-table/types.ts +12 -1
- package/src/components/ui/filter-modal.tsx +4 -1
- package/src/components/ui/floating-tooltip.tsx +11 -5
- package/src/components/ui/service-card.tsx +18 -3
- package/src/components/ui/tag.tsx +34 -1
- package/src/components/ui/truncate-text.tsx +20 -0
- package/dist/chunk-NBKOPZQA.cjs.map +0 -1
- package/dist/chunk-UNQ3BINL.js.map +0 -1
- /package/dist/{chunk-AQAZTEI2.js.map → chunk-4SR2QHMB.js.map} +0 -0
- /package/dist/{chunk-G5LTKJ54.js.map → chunk-BZF63JXP.js.map} +0 -0
- /package/dist/{chunk-O2XKSMIK.js.map → chunk-CLQEBWV7.js.map} +0 -0
- /package/dist/{chunk-P7IOCYGD.js.map → chunk-F7AD5CUU.js.map} +0 -0
- /package/dist/{chunk-X26TVHZN.js.map → chunk-FTOVZ7O2.js.map} +0 -0
- /package/dist/{chunk-JCISIQHT.js.map → chunk-G43CEISB.js.map} +0 -0
- /package/dist/{chunk-FRJ2YKAJ.js.map → chunk-HR6F6IP4.js.map} +0 -0
- /package/dist/{chunk-7HCAAZQ7.js.map → chunk-OAWFYZZT.js.map} +0 -0
- /package/dist/{chunk-VPQBQJ4S.js.map → chunk-SFJRMBJC.js.map} +0 -0
- /package/dist/{chunk-6TOLZW2V.js.map → chunk-VCPTU4IW.js.map} +0 -0
- /package/dist/{chunk-QTJXAVZ3.js.map → chunk-VO2ZONMR.js.map} +0 -0
- /package/dist/{chunk-XOF453AB.js.map → chunk-VYEDTVIZ.js.map} +0 -0
- /package/dist/{chunk-BRRHC7GH.js.map → chunk-XV5SXALY.js.map} +0 -0
|
@@ -19,6 +19,8 @@ export interface DataTableColumnFilterProps {
|
|
|
19
19
|
placement?: 'bottom-start' | 'bottom-end' | 'bottom'
|
|
20
20
|
label: string
|
|
21
21
|
align?: 'left' | 'center' | 'right'
|
|
22
|
+
/** Options are still loading — see `meta.filter.pending`. */
|
|
23
|
+
pending?: boolean
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/** Filter dropdown wrapper that reads/writes TanStack column filter state. */
|
|
@@ -28,6 +30,7 @@ export function DataTableColumnFilter({
|
|
|
28
30
|
placement = 'bottom-start',
|
|
29
31
|
label,
|
|
30
32
|
align = 'left',
|
|
33
|
+
pending = false,
|
|
31
34
|
}: DataTableColumnFilterProps) {
|
|
32
35
|
const currentValue = column.getFilterValue() as string[] | undefined
|
|
33
36
|
const activeCount = currentValue?.length ?? 0
|
|
@@ -62,14 +65,17 @@ export function DataTableColumnFilter({
|
|
|
62
65
|
column.setFilterValue(undefined)
|
|
63
66
|
}, [column])
|
|
64
67
|
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
// the
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
68
|
+
// Nothing to choose from. Two ways to get here, and they look different:
|
|
69
|
+
//
|
|
70
|
+
// - `pending`: the options are in flight. The funnel is drawn but inert, so
|
|
71
|
+
// it does not appear out of nowhere when the query answers — the state a
|
|
72
|
+
// skeleton, or a table whose facets load separately from its rows, asks for.
|
|
73
|
+
// - otherwise: there are no options and there will be none. No funnel: a
|
|
74
|
+
// control that cannot be opened reads as a broken one, and the design shows
|
|
75
|
+
// the bare label. `meta.filter` still being declared is what keeps the
|
|
76
|
+
// header cell alive on tablet (below `lg` only filterable cells show) and
|
|
77
|
+
// keeps its 48px box — only the glyph goes.
|
|
78
|
+
const isEmpty = options.length === 0
|
|
73
79
|
|
|
74
80
|
const trigger = (
|
|
75
81
|
<div
|
|
@@ -82,36 +88,39 @@ export function DataTableColumnFilter({
|
|
|
82
88
|
// the row it belongs to.
|
|
83
89
|
'group flex w-full items-center gap-[var(--spacing-system-xsf)] h-12 rounded-sm transition-colors duration-200 select-none',
|
|
84
90
|
alignJustify(align),
|
|
85
|
-
|
|
91
|
+
isEmpty ? 'cursor-default' : 'cursor-pointer',
|
|
86
92
|
)}
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
// Only a real control announces itself as one. With no options this box
|
|
94
|
+
// is the plain header label the non-filterable columns beside it render.
|
|
95
|
+
aria-label={isEmpty ? undefined : `Filter by ${label}`}
|
|
89
96
|
>
|
|
90
97
|
<span
|
|
91
98
|
className={cn(
|
|
92
99
|
'text-h5 text-ods-text-secondary whitespace-nowrap transition-colors duration-200',
|
|
93
|
-
!
|
|
100
|
+
!isEmpty && 'group-hover:text-ods-text-primary',
|
|
94
101
|
)}
|
|
95
102
|
>
|
|
96
103
|
{label}
|
|
97
104
|
</span>
|
|
98
|
-
{/*
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
{/* Kept while PENDING — it holds its own width, so the label does not
|
|
106
|
+
shift sideways the moment the options resolve — and dropped once we
|
|
107
|
+
know there are none. */}
|
|
108
|
+
{(!isEmpty || pending) && (
|
|
109
|
+
<Filter02Icon
|
|
110
|
+
className={cn(
|
|
111
|
+
'w-4 h-4 transition-colors',
|
|
112
|
+
activeCount > 0 && 'text-ods-accent',
|
|
113
|
+
activeCount === 0 && 'text-ods-text-secondary',
|
|
114
|
+
activeCount === 0 && !isEmpty && 'group-hover:text-ods-text-primary',
|
|
115
|
+
)}
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
109
118
|
</div>
|
|
110
119
|
)
|
|
111
120
|
|
|
112
121
|
// Same box, no dropdown: opening onto an empty list would be a worse answer
|
|
113
122
|
// than not opening at all.
|
|
114
|
-
if (
|
|
123
|
+
if (isEmpty) {
|
|
115
124
|
return <div className="block w-full">{trigger}</div>
|
|
116
125
|
}
|
|
117
126
|
|
|
@@ -240,6 +240,7 @@ function HeaderCell({ header, sort, onSortChange }: HeaderCellProps) {
|
|
|
240
240
|
column={column}
|
|
241
241
|
options={meta!.filter!.options}
|
|
242
242
|
placement={meta!.filter!.placement}
|
|
243
|
+
pending={meta!.filter!.pending}
|
|
243
244
|
label={resolveHeaderLabel(header)}
|
|
244
245
|
align={align}
|
|
245
246
|
/>
|
|
@@ -22,10 +22,21 @@ declare module '@tanstack/react-table' {
|
|
|
22
22
|
* (e.g. the date filter calendar trigger).
|
|
23
23
|
*/
|
|
24
24
|
alwaysShowHeader?: boolean
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* If present, the column filters: the header cell survives on tablet and,
|
|
27
|
+
* when there is something to choose from, renders the funnel + dropdown.
|
|
28
|
+
*
|
|
29
|
+
* An EMPTY `options` list draws no funnel at all — a control that cannot be
|
|
30
|
+
* opened is worse than no control, and every table in the design shows the
|
|
31
|
+
* bare label there. Set `pending` for the other empty case, "the options are
|
|
32
|
+
* still loading": that one keeps the funnel, so it does not appear out of
|
|
33
|
+
* nowhere the moment the query answers.
|
|
34
|
+
*/
|
|
26
35
|
filter?: {
|
|
27
36
|
options: DataTableFilterOption[]
|
|
28
37
|
placement?: 'bottom-start' | 'bottom-end' | 'bottom'
|
|
38
|
+
/** Options are in flight — draw the funnel, inert, instead of hiding it. */
|
|
39
|
+
pending?: boolean
|
|
29
40
|
}
|
|
30
41
|
/**
|
|
31
42
|
* Opt-in: header renders the sort indicator and clicks toggle column sort.
|
|
@@ -236,8 +236,11 @@ export function FilterModal({
|
|
|
236
236
|
</div>
|
|
237
237
|
)}
|
|
238
238
|
|
|
239
|
-
{/* Filter groups
|
|
239
|
+
{/* Filter groups. A group with no options is skipped, not drawn as an
|
|
240
|
+
empty box: same rule the column funnels follow — a control that
|
|
241
|
+
cannot be used is worse than no control. */}
|
|
240
242
|
{filterGroups.map((group) => {
|
|
243
|
+
if (group.options.length === 0) return null
|
|
241
244
|
const groupSelection = selectedFilters[group.id] || []
|
|
242
245
|
return (
|
|
243
246
|
<div key={group.id} className="flex flex-col gap-1">
|
|
@@ -27,12 +27,17 @@ interface FloatingTooltipProps {
|
|
|
27
27
|
/** Disable the tooltip without unmounting the trigger wrapper. */
|
|
28
28
|
disabled?: boolean
|
|
29
29
|
/**
|
|
30
|
-
* Classes for the trigger wrapper — the
|
|
31
|
-
* `children` to anchor the tooltip. That
|
|
32
|
-
* caller's layout, so anything the child needed there (`min-w-0`, `flex-1`)
|
|
30
|
+
* Classes for the trigger wrapper — the element this component puts around
|
|
31
|
+
* `children` to anchor the tooltip. That element becomes the flex/grid item in
|
|
32
|
+
* the caller's layout, so anything the child needed there (`min-w-0`, `flex-1`)
|
|
33
33
|
* must move onto it. Omit it and the wrapper carries no class, exactly as before.
|
|
34
34
|
*/
|
|
35
35
|
triggerClassName?: string
|
|
36
|
+
/**
|
|
37
|
+
* Trigger wrapper element. Default `'div'`. Use `'span'` where a block element
|
|
38
|
+
* is invalid HTML — inside a `<p>`, a heading, or another `<span>`.
|
|
39
|
+
*/
|
|
40
|
+
as?: 'div' | 'span'
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
// Parse colored text markup like [YELLOW]text[/YELLOW] into JSX
|
|
@@ -93,6 +98,7 @@ export function FloatingTooltip({
|
|
|
93
98
|
delayDuration = 0,
|
|
94
99
|
disabled = false,
|
|
95
100
|
triggerClassName,
|
|
101
|
+
as: TriggerTag = 'div',
|
|
96
102
|
}: FloatingTooltipProps) {
|
|
97
103
|
const [isOpen, setIsOpen] = React.useState(false)
|
|
98
104
|
const arrowRef = React.useRef<HTMLDivElement>(null)
|
|
@@ -159,9 +165,9 @@ export function FloatingTooltip({
|
|
|
159
165
|
|
|
160
166
|
return (
|
|
161
167
|
<>
|
|
162
|
-
<
|
|
168
|
+
<TriggerTag ref={refs.setReference} className={triggerClassName} {...getReferenceProps()}>
|
|
163
169
|
{children}
|
|
164
|
-
</
|
|
170
|
+
</TriggerTag>
|
|
165
171
|
<FloatingPortal>
|
|
166
172
|
{isOpen && (
|
|
167
173
|
<div
|
|
@@ -8,6 +8,9 @@ import { CheckIcon } from '../icons-v2-generated/signs-and-symbols'
|
|
|
8
8
|
import { ExternalLink } from 'lucide-react'
|
|
9
9
|
import { OpenFrameLogo } from '../..'
|
|
10
10
|
import { useCopyToClipboard } from '../../hooks/use-copy-to-clipboard'
|
|
11
|
+
import { useIsTruncated } from '../../hooks/ui/use-is-truncated'
|
|
12
|
+
import { FloatingTooltip } from './floating-tooltip'
|
|
13
|
+
import { TruncateText } from './truncate-text'
|
|
11
14
|
|
|
12
15
|
export type ServiceCardRowAction = {
|
|
13
16
|
copy?: boolean
|
|
@@ -61,9 +64,11 @@ export function ServiceCard({ title, subtitle, icon, tag, rows, className }: Ser
|
|
|
61
64
|
{resolvedIcon}
|
|
62
65
|
</div>
|
|
63
66
|
<div className="min-w-0">
|
|
64
|
-
<
|
|
67
|
+
<TruncateText variant="h3">{title}</TruncateText>
|
|
65
68
|
{subtitle && (
|
|
66
|
-
<
|
|
69
|
+
<TruncateText variant="h6" tone="secondary">
|
|
70
|
+
{subtitle}
|
|
71
|
+
</TruncateText>
|
|
67
72
|
)}
|
|
68
73
|
</div>
|
|
69
74
|
</div>
|
|
@@ -87,6 +92,7 @@ export function ServiceCard({ title, subtitle, icon, tag, rows, className }: Ser
|
|
|
87
92
|
function ServiceCardRowItem({ row }: { row: ServiceCardRow }) {
|
|
88
93
|
const [revealed, setRevealed] = useState(false)
|
|
89
94
|
const { copy, copied } = useCopyToClipboard()
|
|
95
|
+
const { ref: valueRef, isTruncated: valueTruncated } = useIsTruncated<HTMLDivElement>(row.value)
|
|
90
96
|
const actions = useMemo<ServiceCardRowAction>(() => ({ copy: true, open: !!row.href, reveal: !!row.isSecret, ...row.actions }), [row])
|
|
91
97
|
|
|
92
98
|
const displayValue = row.isSecret ? <MaskedValue value={row.value} isRevealed={revealed} /> : <span>{row.value}</span>
|
|
@@ -104,7 +110,16 @@ function ServiceCardRowItem({ row }: { row: ServiceCardRow }) {
|
|
|
104
110
|
<div className="w-20 md:w-24 shrink-0 text-h6 text-ods-text-primary">{row.label}</div>
|
|
105
111
|
)}
|
|
106
112
|
<div className={cn('flex-1 h-12 rounded-md border border-ods-border bg-ods-bg px-3 md:px-4 flex items-center justify-between min-w-0', row.monospace ? 'font-mono' : '')}>
|
|
107
|
-
|
|
113
|
+
{/* No tooltip while a secret is masked — the old native `title` leaked the raw value on hover. */}
|
|
114
|
+
<FloatingTooltip
|
|
115
|
+
content={row.value}
|
|
116
|
+
side="top"
|
|
117
|
+
disabled={!valueTruncated || (row.isSecret && !revealed)}
|
|
118
|
+
triggerClassName="min-w-0"
|
|
119
|
+
className="max-w-xs [overflow-wrap:anywhere]"
|
|
120
|
+
>
|
|
121
|
+
<div ref={valueRef} className="truncate text-ods-text-primary">{displayValue}</div>
|
|
122
|
+
</FloatingTooltip>
|
|
108
123
|
<div className="flex items-center gap-2 pl-3 flex-shrink-0">
|
|
109
124
|
{actions.reveal && (
|
|
110
125
|
<button
|
|
@@ -4,6 +4,8 @@ import React from "react"
|
|
|
4
4
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
5
|
import { XmarkCircleIcon } from "../icons-v2-generated/signs-and-symbols/xmark-circle-icon"
|
|
6
6
|
import { cn } from "../../utils/cn"
|
|
7
|
+
import { useIsTruncated } from "../../hooks/ui/use-is-truncated"
|
|
8
|
+
import { FloatingTooltip } from "./floating-tooltip"
|
|
7
9
|
|
|
8
10
|
const tagVariants = cva(
|
|
9
11
|
[
|
|
@@ -107,6 +109,37 @@ export interface TagProps
|
|
|
107
109
|
as?: 'div' | 'span'
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
/**
|
|
113
|
+
* The tag's label slot. String labels get a `FloatingTooltip` with the full
|
|
114
|
+
* value when (and only when) the text is actually clipped — replacing the old
|
|
115
|
+
* native `title`, which showed with the OS delay, unstyled, and even when
|
|
116
|
+
* nothing was truncated. Node labels render exactly as before: the caller owns
|
|
117
|
+
* their content (and any tooltip), so none is added here.
|
|
118
|
+
* The trigger is a `span` so the tag stays valid phrasing content (`as="span"`
|
|
119
|
+
* exists precisely for tags inside a `<p>`).
|
|
120
|
+
*/
|
|
121
|
+
function TagLabel({ label, labelClassName }: { label: React.ReactNode; labelClassName?: string }) {
|
|
122
|
+
const isString = typeof label === 'string'
|
|
123
|
+
const { ref, isTruncated } = useIsTruncated<HTMLSpanElement>(isString ? label : null)
|
|
124
|
+
|
|
125
|
+
if (!isString) {
|
|
126
|
+
return <span className={cn("truncate", labelClassName)}>{label}</span>
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<FloatingTooltip
|
|
131
|
+
content={label}
|
|
132
|
+
side="top"
|
|
133
|
+
disabled={!isTruncated}
|
|
134
|
+
as="span"
|
|
135
|
+
triggerClassName="min-w-0 max-w-full"
|
|
136
|
+
className="max-w-xs whitespace-pre-line [overflow-wrap:anywhere]"
|
|
137
|
+
>
|
|
138
|
+
<span ref={ref} className={cn("truncate block", labelClassName)}>{label}</span>
|
|
139
|
+
</FloatingTooltip>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
110
143
|
function Tag({
|
|
111
144
|
label,
|
|
112
145
|
variant,
|
|
@@ -134,7 +167,7 @@ function Tag({
|
|
|
134
167
|
{icon}
|
|
135
168
|
</span>
|
|
136
169
|
)}
|
|
137
|
-
<
|
|
170
|
+
<TagLabel label={label} labelClassName={labelClassName} />
|
|
138
171
|
{onClose && (
|
|
139
172
|
<button
|
|
140
173
|
type="button"
|
|
@@ -26,6 +26,19 @@ export interface TruncateTextProps {
|
|
|
26
26
|
tone?: TruncateTextTone
|
|
27
27
|
/** Force the monospace (heading) font family — preserves the variant's size while swapping family. */
|
|
28
28
|
mono?: boolean
|
|
29
|
+
/**
|
|
30
|
+
* Extra classes for the tooltip trigger wrapper — the element that becomes the
|
|
31
|
+
* flex/grid item in the caller's layout (e.g. `flex-1`). Merged after the
|
|
32
|
+
* built-in `min-w-0 max-w-full`, which is what lets the trigger shrink inside
|
|
33
|
+
* flex rows so the text ellipsizes instead of clipping.
|
|
34
|
+
*/
|
|
35
|
+
triggerClassName?: string
|
|
36
|
+
/**
|
|
37
|
+
* Trigger wrapper element. Default `'div'`. Use `'span'` where a block element
|
|
38
|
+
* is invalid HTML — inside a `<p>`, a heading, or another `<span>`; the span
|
|
39
|
+
* trigger is `inline-block` so the inner truncation still measures.
|
|
40
|
+
*/
|
|
41
|
+
as?: 'div' | 'span'
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
const VARIANT_CLASS: Record<TruncateTextVariant, string> = {
|
|
@@ -69,6 +82,8 @@ export function TruncateText({
|
|
|
69
82
|
variant = 'h4',
|
|
70
83
|
tone = 'primary',
|
|
71
84
|
mono = false,
|
|
85
|
+
triggerClassName,
|
|
86
|
+
as = 'div',
|
|
72
87
|
}: TruncateTextProps) {
|
|
73
88
|
const isMultiLine = lines > 1
|
|
74
89
|
const { ref, isTruncated } = useIsTruncated<HTMLSpanElement>(children, { multiline: isMultiLine })
|
|
@@ -83,6 +98,11 @@ export function TruncateText({
|
|
|
83
98
|
side={side}
|
|
84
99
|
disabled={!isTruncated}
|
|
85
100
|
className="max-w-xs whitespace-pre-line [overflow-wrap:anywhere]"
|
|
101
|
+
as={as}
|
|
102
|
+
// min-w-0: a flex/grid item's automatic minimum size is content-based when
|
|
103
|
+
// overflow is visible, so without this the trigger refuses to shrink and the
|
|
104
|
+
// text clips with no ellipsis and no tooltip.
|
|
105
|
+
triggerClassName={cn('min-w-0 max-w-full', as === 'span' && 'inline-block align-bottom', triggerClassName)}
|
|
86
106
|
>
|
|
87
107
|
<span
|
|
88
108
|
ref={ref}
|