@flamingo-stack/openframe-frontend-core 0.0.202 → 0.0.203-snapshot.20260522034243
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-OII2IERE.cjs → chunk-25LVV26X.cjs} +4 -4
- package/dist/chunk-25LVV26X.cjs.map +1 -0
- package/dist/{chunk-55HF462A.js → chunk-CPXLQ57U.js} +6 -7
- package/dist/chunk-CPXLQ57U.js.map +1 -0
- package/dist/{chunk-JIKTMXTZ.cjs → chunk-QQONFWAN.cjs} +945 -784
- package/dist/chunk-QQONFWAN.cjs.map +1 -0
- package/dist/{chunk-3B43AHYE.cjs → chunk-RMB5DVED.cjs} +6 -7
- package/dist/chunk-RMB5DVED.cjs.map +1 -0
- package/dist/{chunk-4ML3NA2L.js → chunk-XGL5FKIK.js} +4 -4
- package/dist/chunk-XGL5FKIK.js.map +1 -0
- package/dist/{chunk-IDULPYOU.js → chunk-ZXDILOFR.js} +1947 -1786
- package/dist/chunk-ZXDILOFR.js.map +1 -0
- package/dist/components/chat/chat-ticket-item.d.ts.map +1 -1
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/features/select-button.d.ts.map +1 -1
- package/dist/components/index.cjs +6 -4
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +5 -3
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/navigation/navigation-sidebar.d.ts.map +1 -1
- package/dist/components/resizable.d.ts +1 -1
- package/dist/components/shared/product-release/product-release-card-skeleton.d.ts.map +1 -1
- package/dist/components/shared/product-release/product-release-card.d.ts.map +1 -1
- package/dist/components/ui/button/split-button.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-row.d.ts +16 -4
- package/dist/components/ui/data-table/data-table-row.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +52 -52
- package/dist/components/ui/file-manager/index.cjs.map +1 -1
- package/dist/components/ui/file-manager/index.js +3 -3
- package/dist/components/ui/file-manager/index.js.map +1 -1
- package/dist/components/ui/floating-tooltip.d.ts +3 -1
- package/dist/components/ui/floating-tooltip.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +6 -4
- 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 +5 -3
- package/dist/components/ui/input-trigger.d.ts.map +1 -1
- package/dist/components/ui/radio-group.d.ts.map +1 -1
- package/dist/components/ui/ticket-info-section.d.ts.map +1 -1
- package/dist/components/ui/ticket-note-card.d.ts.map +1 -1
- package/dist/components/ui/truncate-text.d.ts +33 -0
- package/dist/components/ui/truncate-text.d.ts.map +1 -0
- package/dist/components/user-summary-stub.d.ts.map +1 -1
- package/dist/hooks/index.cjs +2 -2
- package/dist/hooks/index.js +1 -1
- package/dist/index.cjs +6 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -3
- package/package.json +1 -1
- package/src/components/chat/chat-container.tsx +2 -2
- package/src/components/chat/chat-ticket-item.tsx +2 -3
- package/src/components/features/board/ticket-card.tsx +2 -2
- package/src/components/features/filters-dropdown.tsx +1 -1
- package/src/components/features/notifications/notification-tile.tsx +2 -2
- package/src/components/features/policy-configuration-panel.tsx +1 -1
- package/src/components/features/push-button-selector.tsx +1 -1
- package/src/components/features/select-button.tsx +2 -3
- package/src/components/features/video-bites-display.tsx +1 -1
- package/src/components/features/waitlist-form.tsx +1 -1
- package/src/components/filter-chip.tsx +1 -1
- package/src/components/layout/title-block.tsx +2 -2
- package/src/components/navigation/header-organization-filter.tsx +1 -1
- package/src/components/navigation/navigation-sidebar.tsx +107 -54
- package/src/components/platform/ScriptInfoSection.tsx +1 -1
- package/src/components/shared/onboarding/onboarding-step-card.tsx +2 -2
- package/src/components/shared/product-release/product-release-card-skeleton.tsx +58 -26
- package/src/components/shared/product-release/product-release-card.tsx +170 -133
- package/src/components/shared/product-release/release-detail-page.tsx +1 -1
- package/src/components/ui/assignee-dropdown.tsx +3 -3
- package/src/components/ui/autocomplete.tsx +2 -2
- package/src/components/ui/button/split-button.tsx +3 -5
- package/src/components/ui/checkbox-block.tsx +1 -1
- package/src/components/ui/data-table/data-table-row.tsx +82 -48
- package/src/components/ui/device-card-compact.tsx +2 -2
- package/src/components/ui/device-card.tsx +2 -2
- package/src/components/ui/entity-image.tsx +1 -1
- package/src/components/ui/field-wrapper.tsx +1 -1
- package/src/components/ui/file-manager/file-manager-table-row.tsx +2 -2
- package/src/components/ui/file-upload.tsx +2 -2
- package/src/components/ui/filter-list.tsx +1 -1
- package/src/components/ui/floating-tooltip.tsx +9 -5
- package/src/components/ui/hidden-tags-popup.tsx +1 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/info-card.tsx +2 -2
- package/src/components/ui/input-trigger.tsx +1 -2
- package/src/components/ui/organization-card.tsx +3 -3
- package/src/components/ui/radio-group.tsx +2 -3
- package/src/components/ui/search-input.tsx +2 -2
- package/src/components/ui/service-card.tsx +3 -3
- package/src/components/ui/tag.tsx +1 -1
- package/src/components/ui/tags-manager.tsx +2 -2
- package/src/components/ui/ticket-attachments-list.tsx +1 -1
- package/src/components/ui/ticket-info-section.tsx +2 -3
- package/src/components/ui/ticket-note-card.tsx +4 -1
- package/src/components/ui/toaster.tsx +3 -3
- package/src/components/ui/truncate-text.tsx +116 -0
- package/src/components/user-summary-stub.tsx +32 -26
- package/src/components/vendor-display-button.tsx +1 -1
- package/src/stories/SplitButton.stories.tsx +7 -1
- package/dist/chunk-3B43AHYE.cjs.map +0 -1
- package/dist/chunk-4ML3NA2L.js.map +0 -1
- package/dist/chunk-55HF462A.js.map +0 -1
- package/dist/chunk-IDULPYOU.js.map +0 -1
- package/dist/chunk-JIKTMXTZ.cjs.map +0 -1
- package/dist/chunk-OII2IERE.cjs.map +0 -1
|
@@ -157,46 +157,60 @@ export function ProductReleaseCard({
|
|
|
157
157
|
(changelogCounts?.breaking ?? 0)
|
|
158
158
|
|
|
159
159
|
// Build the metadata-grid cell array — mirrors the hub's
|
|
160
|
-
// EntityAuthorCard composition
|
|
161
|
-
//
|
|
162
|
-
//
|
|
160
|
+
// EntityAuthorCard composition. ALWAYS render all 3 value cells
|
|
161
|
+
// (Type / Status / Released) — missing values render as a plain
|
|
162
|
+
// em-dash + label so the grid keeps a fixed 4-cell shape (matching
|
|
163
|
+
// the skeleton). The Author cell is also always rendered below
|
|
164
|
+
// (effectiveAuthor falls back to a placeholder shape). This is
|
|
165
|
+
// load-to-resolve baseline parity: any conditional cell would
|
|
166
|
+
// introduce a reflow when the skeleton resolves.
|
|
167
|
+
//
|
|
168
|
+
// Plan note: em-dash placeholders read as plain text (NOT a colored
|
|
169
|
+
// StatusBadge for the Type cell) so empty badges don't look broken
|
|
170
|
+
// next to populated badges.
|
|
163
171
|
type ValueCell = {
|
|
164
172
|
value: string
|
|
165
173
|
label: string
|
|
166
174
|
uppercase: boolean
|
|
167
175
|
colorScheme?: 'error' | 'cyan' | 'success' | 'warning'
|
|
168
176
|
}
|
|
169
|
-
const valueCells: ValueCell[] = [
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
177
|
+
const valueCells: ValueCell[] = [
|
|
178
|
+
releaseType && releaseTypeBadgeColor
|
|
179
|
+
? {
|
|
180
|
+
value: releaseType.toUpperCase(),
|
|
181
|
+
label: 'Type',
|
|
182
|
+
uppercase: true,
|
|
183
|
+
colorScheme: releaseTypeBadgeColor,
|
|
184
|
+
}
|
|
185
|
+
: { value: '—', label: 'Type', uppercase: false },
|
|
186
|
+
releaseStatus
|
|
187
|
+
? {
|
|
188
|
+
value: releaseStatus.toUpperCase(),
|
|
189
|
+
label: 'Status',
|
|
190
|
+
uppercase: true,
|
|
191
|
+
}
|
|
192
|
+
: { value: '—', label: 'Status', uppercase: false },
|
|
193
|
+
formattedDate
|
|
194
|
+
? {
|
|
195
|
+
value: formattedDate,
|
|
196
|
+
label: 'Released',
|
|
197
|
+
uppercase: false,
|
|
198
|
+
}
|
|
199
|
+
: { value: '—', label: 'Released', uppercase: false },
|
|
200
|
+
]
|
|
201
|
+
// EMPTY_AUTHOR_PLACEHOLDER shape — mirrors the hub's
|
|
202
|
+
// EMPTY_AUTHOR_PLACEHOLDER constant exported from
|
|
203
|
+
// components/shared/entity-author-card.tsx (hub can't be imported
|
|
204
|
+
// here; the two are kept in lockstep per the inline-duplication
|
|
205
|
+
// policy documented in the catalog branch comment above).
|
|
206
|
+
const effectiveAuthor = author?.full_name
|
|
207
|
+
? author
|
|
208
|
+
: { full_name: '—', avatar_url: null, job_title: 'Unknown' }
|
|
209
|
+
// Fixed 4-cell grid (Type / Status / Released / Author) so the
|
|
210
|
+
// skeleton's shape matches the loaded card exactly. The earlier
|
|
211
|
+
// dynamic `gridColsClass` ternary collapsed missing cells and
|
|
212
|
+
// caused 28-56px reflow on resolve.
|
|
213
|
+
const gridColsClass = 'md:grid-cols-4'
|
|
200
214
|
const dividerClass = 'border-b md:border-b-0 md:border-r border-ods-border'
|
|
201
215
|
|
|
202
216
|
const frameClass = cn(
|
|
@@ -242,107 +256,130 @@ export function ProductReleaseCard({
|
|
|
242
256
|
v{version}
|
|
243
257
|
</span>
|
|
244
258
|
</div>
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
259
|
+
{/* Title — reserve a fixed 2-line height so cards with
|
|
260
|
+
1-line titles don't shrink and the catalog skeleton-to-
|
|
261
|
+
content transition is shift-free. Mirrors the
|
|
262
|
+
onboarding-guide catalog card. */}
|
|
263
|
+
<div className="min-h-[60px] md:min-h-[72px] flex items-start mb-3">
|
|
264
|
+
<h3 className="font-['Azeret_Mono'] font-semibold text-xl md:text-2xl text-ods-text-primary leading-tight line-clamp-2">
|
|
265
|
+
{title}
|
|
266
|
+
</h3>
|
|
267
|
+
</div>
|
|
268
|
+
{/* Summary — fixed 3-line height. `line-clamp-3` caps long
|
|
269
|
+
summaries at 3 lines; `min-h` reserves the same vertical
|
|
270
|
+
space when content is shorter, so the catalog grid stays
|
|
271
|
+
row-consistent regardless of per-card content length.
|
|
272
|
+
Heights derived from text-sm md:text-base × leading-relaxed
|
|
273
|
+
(1.625): 14×1.625×3 ≈ 68 px mobile, 16×1.625×3 ≈ 78 px desktop. */}
|
|
274
|
+
<div className="min-h-[68px] md:min-h-[78px]">
|
|
275
|
+
<p className="font-['DM_Sans'] text-sm md:text-base text-ods-text-secondary leading-relaxed line-clamp-3">
|
|
276
|
+
{summary ?? ''}
|
|
251
277
|
</p>
|
|
252
|
-
|
|
278
|
+
</div>
|
|
253
279
|
</div>
|
|
254
280
|
</div>
|
|
255
281
|
|
|
256
|
-
{/* CHANGELOG STRIP —
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
{/* CHANGELOG STRIP — ALWAYS rendered so the skeleton's
|
|
283
|
+
always-on changelog placeholder matches the loaded shape
|
|
284
|
+
(zero reflow on resolve). When `totalChangelog === 0`, an
|
|
285
|
+
empty-state line takes the same vertical space as the
|
|
286
|
+
populated row. */}
|
|
287
|
+
<div className="border-t border-ods-border pt-3 flex flex-wrap items-center gap-x-4 gap-y-1.5 font-['DM_Sans'] text-sm text-ods-text-secondary">
|
|
288
|
+
{totalChangelog > 0 && changelogCounts ? (
|
|
289
|
+
<>
|
|
290
|
+
{changelogCounts.features > 0 && (
|
|
291
|
+
<span className="inline-flex items-center gap-1.5">
|
|
292
|
+
<Sparkles className="w-3.5 h-3.5" />
|
|
293
|
+
{changelogCounts.features} {changelogCounts.features === 1 ? 'feature' : 'features'}
|
|
294
|
+
</span>
|
|
295
|
+
)}
|
|
296
|
+
{changelogCounts.fixes > 0 && (
|
|
297
|
+
<span className="inline-flex items-center gap-1.5">
|
|
298
|
+
<Wrench className="w-3.5 h-3.5" />
|
|
299
|
+
{changelogCounts.fixes} {changelogCounts.fixes === 1 ? 'fix' : 'fixes'}
|
|
300
|
+
</span>
|
|
301
|
+
)}
|
|
302
|
+
{changelogCounts.improvements > 0 && (
|
|
303
|
+
<span className="inline-flex items-center gap-1.5">
|
|
304
|
+
<TrendingUp className="w-3.5 h-3.5" />
|
|
305
|
+
{changelogCounts.improvements} {changelogCounts.improvements === 1 ? 'improvement' : 'improvements'}
|
|
306
|
+
</span>
|
|
307
|
+
)}
|
|
308
|
+
{changelogCounts.breaking > 0 && (
|
|
309
|
+
<span className="inline-flex items-center gap-1.5 text-[var(--ods-attention-yellow-warning)]">
|
|
310
|
+
<AlertTriangle className="w-3.5 h-3.5" />
|
|
311
|
+
{changelogCounts.breaking} breaking
|
|
312
|
+
</span>
|
|
313
|
+
)}
|
|
314
|
+
</>
|
|
315
|
+
) : (
|
|
316
|
+
<span className="text-sm text-ods-text-secondary">No changelog entries yet</span>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
285
319
|
|
|
286
|
-
{/* METADATA GRID FOOTER —
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
>
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
</div>
|
|
324
|
-
))}
|
|
325
|
-
{hasAuthorCell && author && (
|
|
326
|
-
<div className="bg-ods-card p-4 flex items-center gap-3">
|
|
327
|
-
<SquareAvatar
|
|
328
|
-
src={author.avatar_url ?? undefined}
|
|
329
|
-
alt={author.full_name}
|
|
330
|
-
fallback={author.full_name.charAt(0).toUpperCase()}
|
|
331
|
-
size="md"
|
|
332
|
-
variant="round"
|
|
333
|
-
/>
|
|
334
|
-
<div className="flex flex-col gap-0 flex-1 min-w-0">
|
|
335
|
-
<p className="text-h3 tracking-[-0.36px] text-ods-text-primary truncate">
|
|
336
|
-
{author.full_name}
|
|
337
|
-
</p>
|
|
338
|
-
<p className="font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary">
|
|
339
|
-
{author.job_title || 'Author'}
|
|
320
|
+
{/* METADATA GRID FOOTER — fixed 4-cell shape (Type / Status /
|
|
321
|
+
Released / Author) so the skeleton mirrors the loaded card
|
|
322
|
+
exactly. Empty value cells render em-dash + label (plain
|
|
323
|
+
text, no colored badge — em-dash badges read as broken next
|
|
324
|
+
to populated ones); the Author cell falls back to the
|
|
325
|
+
EMPTY_AUTHOR_PLACEHOLDER shape declared above. */}
|
|
326
|
+
<div
|
|
327
|
+
className={cn(
|
|
328
|
+
'grid grid-cols-1',
|
|
329
|
+
gridColsClass,
|
|
330
|
+
'border border-ods-border rounded-md overflow-hidden w-full',
|
|
331
|
+
)}
|
|
332
|
+
>
|
|
333
|
+
{valueCells.map((cell, i) => (
|
|
334
|
+
<div
|
|
335
|
+
key={`${cell.label}-${i}`}
|
|
336
|
+
className={cn('bg-ods-card p-4 flex flex-col gap-3', dividerClass)}
|
|
337
|
+
>
|
|
338
|
+
<div className="flex flex-col gap-0">
|
|
339
|
+
{cell.colorScheme ? (
|
|
340
|
+
<StatusBadge
|
|
341
|
+
text={cell.value}
|
|
342
|
+
variant="card"
|
|
343
|
+
colorScheme={cell.colorScheme}
|
|
344
|
+
singleLine
|
|
345
|
+
className="self-start"
|
|
346
|
+
/>
|
|
347
|
+
) : (
|
|
348
|
+
<p
|
|
349
|
+
className={cn(
|
|
350
|
+
'text-h4',
|
|
351
|
+
// Em-dash placeholder reads as secondary text;
|
|
352
|
+
// populated values stay primary.
|
|
353
|
+
cell.value === '—' ? 'text-ods-text-secondary' : 'text-ods-text-primary',
|
|
354
|
+
)}
|
|
355
|
+
>
|
|
356
|
+
{cell.uppercase ? cell.value.toLocaleUpperCase() : cell.value}
|
|
340
357
|
</p>
|
|
341
|
-
|
|
358
|
+
)}
|
|
359
|
+
<p className="font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary">
|
|
360
|
+
{cell.label}
|
|
361
|
+
</p>
|
|
342
362
|
</div>
|
|
343
|
-
|
|
363
|
+
</div>
|
|
364
|
+
))}
|
|
365
|
+
<div className="bg-ods-card p-4 flex items-center gap-3">
|
|
366
|
+
<SquareAvatar
|
|
367
|
+
src={effectiveAuthor.avatar_url ?? undefined}
|
|
368
|
+
alt={effectiveAuthor.full_name}
|
|
369
|
+
fallback={effectiveAuthor.full_name.charAt(0).toUpperCase()}
|
|
370
|
+
size="md"
|
|
371
|
+
variant="round"
|
|
372
|
+
/>
|
|
373
|
+
<div className="flex flex-col gap-0 flex-1 min-w-0">
|
|
374
|
+
<p className="text-h3 tracking-[-0.36px] text-ods-text-primary truncate">
|
|
375
|
+
{effectiveAuthor.full_name}
|
|
376
|
+
</p>
|
|
377
|
+
<p className="font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary">
|
|
378
|
+
{effectiveAuthor.job_title || 'Author'}
|
|
379
|
+
</p>
|
|
380
|
+
</div>
|
|
344
381
|
</div>
|
|
345
|
-
|
|
382
|
+
</div>
|
|
346
383
|
|
|
347
384
|
{typeof viewCount === 'number' && viewCount > 0 && (
|
|
348
385
|
<div className="flex items-center gap-1.5 text-xs text-ods-text-secondary">
|
|
@@ -463,7 +500,7 @@ export function ProductReleaseCard({
|
|
|
463
500
|
as the loaded text — zero load-to-resolve baseline shift. */}
|
|
464
501
|
<span className="flex min-w-0 flex-1 flex-col gap-0.5 min-h-14">
|
|
465
502
|
<span className="flex items-center gap-2 min-w-0 h-5">
|
|
466
|
-
<span className="truncate text-sm font-semibold leading-5 text-ods-text-primary min-w-0">
|
|
503
|
+
<span className="truncate text-sm font-semibold leading-5 text-ods-text-primary min-w-0" title={title}>
|
|
467
504
|
{title}
|
|
468
505
|
</span>
|
|
469
506
|
{version ? (
|
|
@@ -478,7 +515,7 @@ export function ProductReleaseCard({
|
|
|
478
515
|
</span>
|
|
479
516
|
</span>
|
|
480
517
|
<span className="flex items-center min-w-0 h-4">
|
|
481
|
-
<span className="truncate text-[11px] leading-4 text-ods-text-secondary/80">
|
|
518
|
+
<span className="truncate text-[11px] leading-4 text-ods-text-secondary/80" title={summary || undefined}>
|
|
482
519
|
{/* The literal between the curly-quote string is U+00A0
|
|
483
520
|
(NBSP). The hub's `COMPACT_CARD_ROW_FILLER` is also
|
|
484
521
|
NBSP; ASCII space here would let React collapse the
|
|
@@ -545,11 +582,11 @@ export function ProductReleaseCard({
|
|
|
545
582
|
{/* Left column - content */}
|
|
546
583
|
<div className="flex-1 w-full md:w-auto min-w-0 flex flex-col justify-center gap-2">
|
|
547
584
|
<div className="min-h-[48px] flex items-center">
|
|
548
|
-
<h3 className="text-h3 text-ods-text-primary tracking-[-0.36px] line-clamp-2">
|
|
585
|
+
<h3 className="text-h3 text-ods-text-primary tracking-[-0.36px] line-clamp-2" title={title}>
|
|
549
586
|
{title}
|
|
550
587
|
</h3>
|
|
551
588
|
</div>
|
|
552
|
-
<p className="text-h4 text-ods-text-secondary line-clamp-3">
|
|
589
|
+
<p className="text-h4 text-ods-text-secondary line-clamp-3" title={summary || ' '}>
|
|
553
590
|
{summary || ' '}
|
|
554
591
|
</p>
|
|
555
592
|
</div>
|
|
@@ -585,11 +622,11 @@ export function ProductReleaseCard({
|
|
|
585
622
|
{/* Left column - content */}
|
|
586
623
|
<div className="flex-1 w-full md:w-auto min-w-0 flex flex-col justify-center gap-2">
|
|
587
624
|
<div className="min-h-[48px] flex items-center">
|
|
588
|
-
<h3 className="text-h3 text-ods-text-primary tracking-[-0.36px] line-clamp-2">
|
|
625
|
+
<h3 className="text-h3 text-ods-text-primary tracking-[-0.36px] line-clamp-2" title={title}>
|
|
589
626
|
{title}
|
|
590
627
|
</h3>
|
|
591
628
|
</div>
|
|
592
|
-
<p className="text-h4 text-ods-text-secondary line-clamp-3">
|
|
629
|
+
<p className="text-h4 text-ods-text-secondary line-clamp-3" title={summary || ' '}>
|
|
593
630
|
{summary || ' '}
|
|
594
631
|
</p>
|
|
595
632
|
</div>
|
|
@@ -282,7 +282,7 @@ export function ReleaseDetailPage({
|
|
|
282
282
|
variant="round"
|
|
283
283
|
/>
|
|
284
284
|
<div className="flex flex-col gap-0 flex-1 min-w-0">
|
|
285
|
-
<p className="text-h3 tracking-[-0.36px] text-ods-text-primary truncate">
|
|
285
|
+
<p className="text-h3 tracking-[-0.36px] text-ods-text-primary truncate" title={author?.full_name || 'Unknown Author'}>
|
|
286
286
|
{author?.full_name || 'Unknown Author'}
|
|
287
287
|
</p>
|
|
288
288
|
<p className="font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary">
|
|
@@ -159,7 +159,7 @@ function CompactAssigneeDropdown({
|
|
|
159
159
|
variant="round"
|
|
160
160
|
className="h-6 w-6 shrink-0"
|
|
161
161
|
/>
|
|
162
|
-
<span className="flex-1 truncate text-h4 text-ods-text-primary">{opt.label}</span>
|
|
162
|
+
<span className="flex-1 truncate text-h4 text-ods-text-primary" title={opt.label}>{opt.label}</span>
|
|
163
163
|
{isCurrent && <CheckIcon className="size-4 shrink-0 text-ods-accent" />}
|
|
164
164
|
</button>
|
|
165
165
|
)
|
|
@@ -194,7 +194,7 @@ function DefaultAssigneeDropdown({
|
|
|
194
194
|
variant="round"
|
|
195
195
|
className="h-6 w-6 shrink-0"
|
|
196
196
|
/>
|
|
197
|
-
<span className="truncate">{opt.label}</span>
|
|
197
|
+
<span className="truncate" title={opt.label}>{opt.label}</span>
|
|
198
198
|
</div>
|
|
199
199
|
)
|
|
200
200
|
}, [])
|
|
@@ -253,7 +253,7 @@ function DefaultAssigneeDropdown({
|
|
|
253
253
|
className="flex items-center gap-[var(--spacing-system-xxs)] cursor-pointer group text-left"
|
|
254
254
|
>
|
|
255
255
|
<PenEditIcon className="size-4 shrink-0 text-ods-text-secondary group-hover:text-ods-accent transition-colors" />
|
|
256
|
-
<span className="text-h4 text-ods-text-primary truncate">{currentAssignee!.name}</span>
|
|
256
|
+
<span className="text-h4 text-ods-text-primary truncate" title={currentAssignee!.name}>{currentAssignee!.name}</span>
|
|
257
257
|
</button>
|
|
258
258
|
</div>
|
|
259
259
|
<span className="text-h6 text-ods-text-secondary truncate">Assigned</span>
|
|
@@ -574,7 +574,7 @@ function AutocompleteInner<T = string>(
|
|
|
574
574
|
)}
|
|
575
575
|
onClick={handleCreate}
|
|
576
576
|
>
|
|
577
|
-
<span className="truncate">+ Create "{inputValue.trim()}"</span>
|
|
577
|
+
<span className="truncate" title={`+ Create "${inputValue.trim()}"`}>+ Create "{inputValue.trim()}"</span>
|
|
578
578
|
</div>
|
|
579
579
|
) : (
|
|
580
580
|
<div className="px-3 py-2 text-ods-text-secondary text-[14px]">
|
|
@@ -607,7 +607,7 @@ function AutocompleteInner<T = string>(
|
|
|
607
607
|
>
|
|
608
608
|
{renderOption ? renderOption(option, isSelected) : (
|
|
609
609
|
<div className="flex items-center justify-between w-full min-w-0">
|
|
610
|
-
<span className="truncate">{option.label}</span>
|
|
610
|
+
<span className="truncate" title={option.label}>{option.label}</span>
|
|
611
611
|
{isSelected && (
|
|
612
612
|
<CheckIcon className="text-ods-accent" size={20} />
|
|
613
613
|
)}
|
|
@@ -16,7 +16,7 @@ const splitHalfBase = [
|
|
|
16
16
|
"whitespace-nowrap transition-colors duration-200",
|
|
17
17
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus focus-visible:z-10",
|
|
18
18
|
"disabled:pointer-events-none",
|
|
19
|
-
"[&_svg]:pointer-events-none [&_svg]:shrink-0
|
|
19
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
20
20
|
]
|
|
21
21
|
|
|
22
22
|
const splitHalfVariants = cva(splitHalfBase, {
|
|
@@ -28,8 +28,8 @@ const splitHalfVariants = cva(splitHalfBase, {
|
|
|
28
28
|
destructive: buttonSurfaceClasses.destructive,
|
|
29
29
|
},
|
|
30
30
|
size: {
|
|
31
|
-
default: "h-12 px-[var(--spacing-system-m)] py-[var(--spacing-system-sf)] text-h3",
|
|
32
|
-
small: "h-6 md:h-8 px-[var(--spacing-system-xs)] text-h5",
|
|
31
|
+
default: "h-10 md:h-12 px-[var(--spacing-system-m)] py-[var(--spacing-system-sf)] text-h3 [&_svg]:h-4 [&_svg]:w-4 md:[&_svg]:h-6 md:[&_svg]:w-6",
|
|
32
|
+
small: "h-6 md:h-8 px-[var(--spacing-system-xs)] text-h5 [&_svg]:h-3 [&_svg]:w-3 md:[&_svg]:h-4 md:[&_svg]:w-4",
|
|
33
33
|
},
|
|
34
34
|
side: { main: "", icon: "" },
|
|
35
35
|
},
|
|
@@ -55,8 +55,6 @@ const splitHalfVariants = cva(splitHalfBase, {
|
|
|
55
55
|
// Icon half: per Figma, narrower than main height (default: 40×48; small: 32×32).
|
|
56
56
|
{ side: "icon", size: "default", class: "w-10 px-0" },
|
|
57
57
|
{ side: "icon", size: "small", class: "w-6 md:w-8 px-0" },
|
|
58
|
-
|
|
59
|
-
{ size: "small", class: "[&_svg]:h-4 [&_svg]:w-4" },
|
|
60
58
|
],
|
|
61
59
|
defaultVariants: { variant: "accent", size: "default", side: "main" },
|
|
62
60
|
})
|
|
@@ -80,7 +80,7 @@ const CheckboxBlock = React.forwardRef<
|
|
|
80
80
|
</div>
|
|
81
81
|
</label>
|
|
82
82
|
{error && (
|
|
83
|
-
<p className="absolute bottom-0 left-0 right-0 translate-y-full text-h6 truncate text-ods-error">
|
|
83
|
+
<p className="absolute bottom-0 left-0 right-0 translate-y-full text-h6 truncate text-ods-error" title={error}>
|
|
84
84
|
{error}
|
|
85
85
|
</p>
|
|
86
86
|
)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link'
|
|
4
|
-
import React, { memo, useCallback, type ReactNode } from 'react'
|
|
4
|
+
import React, { memo, useCallback, useRef, type ReactNode } from 'react'
|
|
5
5
|
import { flexRender, type Row } from '@tanstack/react-table'
|
|
6
6
|
import { cn } from '../../../utils/cn'
|
|
7
7
|
import { ROW_HEIGHT_DESKTOP } from './data-table-skeleton'
|
|
@@ -19,16 +19,28 @@ export interface DataTableRowProps<T> {
|
|
|
19
19
|
/**
|
|
20
20
|
* Click-bubbling protocol: any element inside a cell that should NOT trigger
|
|
21
21
|
* `onRowClick` / row navigation must carry the `data-no-row-click` attribute.
|
|
22
|
-
* The row checks `target.closest('[data-no-row-click]')`
|
|
23
|
-
* `onClick
|
|
24
|
-
*
|
|
22
|
+
* The row checks `target.closest('[data-no-row-click]')` and short-circuits:
|
|
23
|
+
* in `onClick` mode it skips the consumer's handler; in link mode (when
|
|
24
|
+
* `href` is set) it calls `e.preventDefault()` so `<Link>` does not navigate.
|
|
25
|
+
*
|
|
26
|
+
* Clicks originating from portaled descendants (e.g. `FloatingTooltip`,
|
|
27
|
+
* dropdown menus rendered through `FloatingPortal`) bubble through React's
|
|
28
|
+
* component tree and reach this handler, but their DOM target lives outside
|
|
29
|
+
* the row subtree. The handler ignores any click whose target is not
|
|
30
|
+
* physically contained within the row element — no `stopPropagation`
|
|
31
|
+
* required at the source.
|
|
32
|
+
*
|
|
33
|
+
* In link mode the row IS the `<Link>` — content lives inside it, not under
|
|
34
|
+
* an absolute overlay — so native browser link behaviour works: hover,
|
|
35
|
+
* right-click "Open in new tab", middle-click, `Cmd+click`, focus outlines,
|
|
36
|
+
* `:visited` styles, etc.
|
|
25
37
|
*
|
|
26
38
|
* Example column with action buttons:
|
|
27
39
|
* ```tsx
|
|
28
40
|
* {
|
|
29
41
|
* id: 'actions',
|
|
30
42
|
* cell: ({ row }) => (
|
|
31
|
-
* <div data-no-row-click className="flex gap-2 justify-end
|
|
43
|
+
* <div data-no-row-click className="flex gap-2 justify-end">
|
|
32
44
|
* <Button onClick={() => edit(row.original)}>Edit</Button>
|
|
33
45
|
* </div>
|
|
34
46
|
* ),
|
|
@@ -49,61 +61,83 @@ function DataTableRowImpl<T>({
|
|
|
49
61
|
className,
|
|
50
62
|
}: DataTableRowProps<T>) {
|
|
51
63
|
const isLinkMode = Boolean(href) && !onClick
|
|
64
|
+
const containerRef = useRef<HTMLElement | null>(null)
|
|
52
65
|
|
|
53
66
|
const handleClick = useCallback(
|
|
54
67
|
(e: React.MouseEvent) => {
|
|
55
68
|
const target = e.target as HTMLElement
|
|
56
|
-
|
|
69
|
+
// React-bubbled events from portaled descendants (tooltips, dropdowns, etc.)
|
|
70
|
+
// reach this handler even though their DOM target lives outside the row.
|
|
71
|
+
// Suppress them — and in link mode, preventDefault so `<Link>` does not navigate.
|
|
72
|
+
if (!containerRef.current?.contains(target)) {
|
|
73
|
+
if (isLinkMode) e.preventDefault()
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
if (target.closest('[data-no-row-click]')) {
|
|
77
|
+
if (isLinkMode) e.preventDefault()
|
|
78
|
+
return
|
|
79
|
+
}
|
|
57
80
|
onClick?.(row.original)
|
|
58
81
|
},
|
|
59
|
-
[onClick, row.original],
|
|
82
|
+
[onClick, row.original, isLinkMode],
|
|
60
83
|
)
|
|
61
84
|
|
|
62
|
-
|
|
85
|
+
const containerClassName = cn(
|
|
86
|
+
'block rounded-md bg-ods-card border border-ods-border overflow-hidden no-underline text-inherit',
|
|
87
|
+
(onClick || isLinkMode) && 'cursor-pointer hover:bg-ods-bg-active transition-colors',
|
|
88
|
+
className,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const cells = (
|
|
63
92
|
<div
|
|
64
93
|
className={cn(
|
|
65
|
-
'
|
|
66
|
-
|
|
67
|
-
'cursor-pointer hover:bg-ods-bg-active transition-colors',
|
|
68
|
-
className,
|
|
94
|
+
'flex items-center gap-[var(--spacing-system-mf)] px-[var(--spacing-system-mf)]',
|
|
95
|
+
compact ? 'py-[var(--spacing-system-xsf)]' : `py-0 ${ROW_HEIGHT_DESKTOP}`,
|
|
69
96
|
)}
|
|
70
|
-
onClick={isLinkMode ? undefined : handleClick}
|
|
71
97
|
>
|
|
72
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
98
|
+
{row.getVisibleCells().map(cell => {
|
|
99
|
+
const meta = cell.column.columnDef.meta
|
|
100
|
+
return (
|
|
101
|
+
<div
|
|
102
|
+
key={cell.id}
|
|
103
|
+
className={cn(
|
|
104
|
+
'flex flex-col overflow-hidden',
|
|
105
|
+
alignJustify(meta?.align),
|
|
106
|
+
meta?.width || 'flex-1 min-w-0',
|
|
107
|
+
meta?.cellClassName,
|
|
108
|
+
getHideClasses(meta?.hideAt),
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
<CellContent>
|
|
112
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
113
|
+
</CellContent>
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
})}
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if (isLinkMode && href) {
|
|
121
|
+
return (
|
|
122
|
+
<Link
|
|
123
|
+
href={href}
|
|
124
|
+
prefetch={false}
|
|
125
|
+
ref={containerRef as React.RefObject<HTMLAnchorElement>}
|
|
126
|
+
className={containerClassName}
|
|
127
|
+
onClick={handleClick}
|
|
86
128
|
>
|
|
87
|
-
{
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
>
|
|
100
|
-
<CellContent>
|
|
101
|
-
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
102
|
-
</CellContent>
|
|
103
|
-
</div>
|
|
104
|
-
)
|
|
105
|
-
})}
|
|
106
|
-
</div>
|
|
129
|
+
{cells}
|
|
130
|
+
</Link>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
ref={containerRef as React.RefObject<HTMLDivElement>}
|
|
137
|
+
className={containerClassName}
|
|
138
|
+
onClick={onClick ? handleClick : undefined}
|
|
139
|
+
>
|
|
140
|
+
{cells}
|
|
107
141
|
</div>
|
|
108
142
|
)
|
|
109
143
|
}
|
|
@@ -114,7 +148,7 @@ export const DataTableRow = memo(DataTableRowImpl) as typeof DataTableRowImpl
|
|
|
114
148
|
function CellContent({ children }: { children: ReactNode }) {
|
|
115
149
|
if (typeof children === 'string' || typeof children === 'number') {
|
|
116
150
|
return (
|
|
117
|
-
<span className="text-h4 text-ods-text-primary truncate">{children}</span>
|
|
151
|
+
<span className="text-h4 text-ods-text-primary truncate" title={String(children)}>{children}</span>
|
|
118
152
|
)
|
|
119
153
|
}
|
|
120
154
|
return <>{children}</>
|