@flamingo-stack/openframe-frontend-core 0.0.201 → 0.0.202-snapshot.20260521221224

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/{chunk-OII2IERE.cjs → chunk-25LVV26X.cjs} +4 -4
  2. package/dist/chunk-25LVV26X.cjs.map +1 -0
  3. package/dist/{chunk-UCY537V4.cjs → chunk-3YH2M76N.cjs} +1565 -1146
  4. package/dist/chunk-3YH2M76N.cjs.map +1 -0
  5. package/dist/{chunk-55HF462A.js → chunk-CPXLQ57U.js} +6 -7
  6. package/dist/chunk-CPXLQ57U.js.map +1 -0
  7. package/dist/{chunk-CSW5GYBU.js → chunk-E6Q6UGDK.js} +4603 -4184
  8. package/dist/chunk-E6Q6UGDK.js.map +1 -0
  9. package/dist/{chunk-3B43AHYE.cjs → chunk-RMB5DVED.cjs} +6 -7
  10. package/dist/chunk-RMB5DVED.cjs.map +1 -0
  11. package/dist/{chunk-4ML3NA2L.js → chunk-XGL5FKIK.js} +4 -4
  12. package/dist/chunk-XGL5FKIK.js.map +1 -0
  13. package/dist/components/chat/approval-request-message.d.ts.map +1 -1
  14. package/dist/components/chat/chat-container.d.ts.map +1 -1
  15. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  16. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  17. package/dist/components/chat/chat-ticket-item.d.ts.map +1 -1
  18. package/dist/components/chat/types/message.types.d.ts +34 -0
  19. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  20. package/dist/components/features/index.cjs +16 -4
  21. package/dist/components/features/index.cjs.map +1 -1
  22. package/dist/components/features/index.d.ts +1 -0
  23. package/dist/components/features/index.d.ts.map +1 -1
  24. package/dist/components/features/index.js +17 -5
  25. package/dist/components/features/select-button.d.ts.map +1 -1
  26. package/dist/components/index.cjs +18 -4
  27. package/dist/components/index.cjs.map +1 -1
  28. package/dist/components/index.js +17 -3
  29. package/dist/components/navigation/index.cjs +4 -4
  30. package/dist/components/navigation/index.js +3 -3
  31. package/dist/components/navigation/navigation-sidebar.d.ts.map +1 -1
  32. package/dist/components/providers/theme-provider.d.ts +69 -0
  33. package/dist/components/providers/theme-provider.d.ts.map +1 -0
  34. package/dist/components/resizable.d.ts +1 -1
  35. package/dist/components/ui/button/split-button.d.ts.map +1 -1
  36. package/dist/components/ui/data-table/data-table-row.d.ts +16 -4
  37. package/dist/components/ui/data-table/data-table-row.d.ts.map +1 -1
  38. package/dist/components/ui/file-manager/index.cjs +52 -52
  39. package/dist/components/ui/file-manager/index.cjs.map +1 -1
  40. package/dist/components/ui/file-manager/index.js +3 -3
  41. package/dist/components/ui/file-manager/index.js.map +1 -1
  42. package/dist/components/ui/floating-tooltip.d.ts +3 -1
  43. package/dist/components/ui/floating-tooltip.d.ts.map +1 -1
  44. package/dist/components/ui/index.cjs +6 -4
  45. package/dist/components/ui/index.cjs.map +1 -1
  46. package/dist/components/ui/index.d.ts +1 -0
  47. package/dist/components/ui/index.d.ts.map +1 -1
  48. package/dist/components/ui/index.js +5 -3
  49. package/dist/components/ui/input-trigger.d.ts.map +1 -1
  50. package/dist/components/ui/radio-group.d.ts.map +1 -1
  51. package/dist/components/ui/simple-markdown-renderer.d.ts.map +1 -1
  52. package/dist/components/ui/ticket-info-section.d.ts.map +1 -1
  53. package/dist/components/ui/ticket-note-card.d.ts.map +1 -1
  54. package/dist/components/ui/truncate-text.d.ts +33 -0
  55. package/dist/components/ui/truncate-text.d.ts.map +1 -0
  56. package/dist/components/user-summary-stub.d.ts.map +1 -1
  57. package/dist/hooks/index.cjs +2 -2
  58. package/dist/hooks/index.js +1 -1
  59. package/dist/index.cjs +18 -4
  60. package/dist/index.cjs.map +1 -1
  61. package/dist/index.js +17 -3
  62. package/package.json +2 -1
  63. package/src/components/chat/approval-request-message.tsx +106 -92
  64. package/src/components/chat/chat-container.tsx +10 -6
  65. package/src/components/chat/chat-message-enhanced.tsx +51 -9
  66. package/src/components/chat/chat-message-list.tsx +27 -19
  67. package/src/components/chat/chat-ticket-item.tsx +2 -3
  68. package/src/components/chat/types/message.types.ts +35 -0
  69. package/src/components/features/board/ticket-card.tsx +2 -2
  70. package/src/components/features/filters-dropdown.tsx +1 -1
  71. package/src/components/features/index.ts +15 -0
  72. package/src/components/features/notifications/notification-tile.tsx +2 -2
  73. package/src/components/features/policy-configuration-panel.tsx +1 -1
  74. package/src/components/features/push-button-selector.tsx +1 -1
  75. package/src/components/features/select-button.tsx +2 -3
  76. package/src/components/features/video-bites-display.tsx +1 -1
  77. package/src/components/features/waitlist-form.tsx +1 -1
  78. package/src/components/filter-chip.tsx +1 -1
  79. package/src/components/layout/title-block.tsx +2 -2
  80. package/src/components/navigation/header-organization-filter.tsx +1 -1
  81. package/src/components/navigation/navigation-sidebar.tsx +107 -54
  82. package/src/components/platform/ScriptInfoSection.tsx +1 -1
  83. package/src/components/providers/theme-provider.tsx +130 -0
  84. package/src/components/shared/onboarding/onboarding-step-card.tsx +2 -2
  85. package/src/components/shared/product-release/product-release-card.tsx +6 -6
  86. package/src/components/shared/product-release/release-detail-page.tsx +1 -1
  87. package/src/components/ui/assignee-dropdown.tsx +3 -3
  88. package/src/components/ui/autocomplete.tsx +2 -2
  89. package/src/components/ui/button/split-button.tsx +3 -5
  90. package/src/components/ui/checkbox-block.tsx +1 -1
  91. package/src/components/ui/data-table/data-table-row.tsx +82 -48
  92. package/src/components/ui/device-card-compact.tsx +2 -2
  93. package/src/components/ui/device-card.tsx +2 -2
  94. package/src/components/ui/entity-image.tsx +1 -1
  95. package/src/components/ui/field-wrapper.tsx +1 -1
  96. package/src/components/ui/file-manager/file-manager-table-row.tsx +2 -2
  97. package/src/components/ui/file-upload.tsx +2 -2
  98. package/src/components/ui/filter-list.tsx +1 -1
  99. package/src/components/ui/floating-tooltip.tsx +9 -5
  100. package/src/components/ui/hidden-tags-popup.tsx +1 -1
  101. package/src/components/ui/index.ts +1 -0
  102. package/src/components/ui/info-card.tsx +2 -2
  103. package/src/components/ui/input-trigger.tsx +1 -2
  104. package/src/components/ui/organization-card.tsx +3 -3
  105. package/src/components/ui/radio-group.tsx +2 -3
  106. package/src/components/ui/search-input.tsx +2 -2
  107. package/src/components/ui/service-card.tsx +3 -3
  108. package/src/components/ui/simple-markdown-renderer.tsx +248 -2
  109. package/src/components/ui/tag.tsx +1 -1
  110. package/src/components/ui/tags-manager.tsx +2 -2
  111. package/src/components/ui/ticket-attachments-list.tsx +1 -1
  112. package/src/components/ui/ticket-info-section.tsx +2 -3
  113. package/src/components/ui/ticket-note-card.tsx +4 -1
  114. package/src/components/ui/toaster.tsx +3 -3
  115. package/src/components/ui/truncate-text.tsx +116 -0
  116. package/src/components/user-summary-stub.tsx +32 -26
  117. package/src/components/vendor-display-button.tsx +1 -1
  118. package/src/stories/SplitButton.stories.tsx +7 -1
  119. package/src/stories/Theme.stories.tsx +350 -0
  120. package/src/styles/README.md +271 -174
  121. package/src/styles/dark_theme.tokens.json +982 -0
  122. package/src/styles/light_theme.tokens.json +982 -0
  123. package/src/styles/ods-colors.css +225 -146
  124. package/src/styles/ods_color_tokens.json +1 -300
  125. package/dist/chunk-3B43AHYE.cjs.map +0 -1
  126. package/dist/chunk-4ML3NA2L.js.map +0 -1
  127. package/dist/chunk-55HF462A.js.map +0 -1
  128. package/dist/chunk-CSW5GYBU.js.map +0 -1
  129. package/dist/chunk-OII2IERE.cjs.map +0 -1
  130. package/dist/chunk-UCY537V4.cjs.map +0 -1
@@ -1,8 +1,8 @@
1
1
  "use client"
2
2
 
3
- import { useCallback, useLayoutEffect, useMemo, useState } from 'react'
3
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'
4
4
  import { useLocalStorage } from '../../hooks/ui/use-local-storage'
5
- import { useLgUp } from '../../hooks/ui/use-media-query'
5
+ import { useLgUp, useMdUp } from '../../hooks/ui/use-media-query'
6
6
  import { NavigationSidebarConfig, NavigationSidebarItem } from '../../types/navigation'
7
7
  import { cn } from '../../utils'
8
8
  import { NavigationSidebarHeader } from './navigation-sidebar-header'
@@ -23,25 +23,57 @@ export interface NavigationSidebarProps {
23
23
  }
24
24
 
25
25
  export function NavigationSidebar({ config, disabled = false }: NavigationSidebarProps) {
26
+ const isMdUp = useMdUp() ?? false
26
27
  const isLgUp = useLgUp() ?? false
27
28
 
28
- // useLocalStorage reads from localStorage first, then falls back to this value
29
- const [minimized, setMinimized] = useLocalStorage<boolean>(
29
+ // Tablet = md viewport but not lg. On tablet the sidebar floats over the
30
+ // content area (overlay) instead of pushing it like on desktop.
31
+ const isTablet = isMdUp && !isLgUp
32
+
33
+ // Desktop preference persists across sessions. Tablet state is in-memory
34
+ // only so entering tablet always starts minimized without clobbering the
35
+ // user's desktop choice.
36
+ const [desktopMinimized, setDesktopMinimized] = useLocalStorage<boolean>(
30
37
  STORAGE_KEY,
31
- !isLgUp || (config.minimized ?? false)
38
+ config.minimized ?? false,
32
39
  )
40
+ const [tabletMinimized, setTabletMinimized] = useState(true)
41
+
42
+ useEffect(() => {
43
+ if (isTablet) setTabletMinimized(true)
44
+ }, [isTablet])
45
+
46
+ const minimized = isTablet ? tabletMinimized : desktopMinimized
33
47
 
34
48
  // Enable transitions only after the correct width is painted
35
49
  const [transitionsEnabled, setTransitionsEnabled] = useState(false)
36
50
 
37
- // On tablet (md but not lg), force minimized layout regardless of stored value
38
- const isMinimized = !isLgUp || minimized
39
- const showLabel = isLgUp && !minimized
51
+ const isOverlayOpen = isTablet && !minimized
52
+
53
+ const showLabel = !minimized
40
54
 
41
55
  const handleToggle = useCallback(() => {
42
- setMinimized(prev => !prev)
56
+ if (isTablet) {
57
+ setTabletMinimized(prev => !prev)
58
+ } else {
59
+ setDesktopMinimized(prev => !prev)
60
+ }
43
61
  config.onToggleMinimized?.()
44
- }, [setMinimized, config])
62
+ }, [isTablet, setDesktopMinimized, config])
63
+
64
+ const closeOverlay = useCallback(() => {
65
+ setTabletMinimized(true)
66
+ }, [])
67
+
68
+ // Dismiss the tablet overlay with Escape so it behaves like a transient panel
69
+ useEffect(() => {
70
+ if (!isOverlayOpen) return
71
+ const handleKeyDown = (e: KeyboardEvent) => {
72
+ if (e.key === 'Escape') closeOverlay()
73
+ }
74
+ document.addEventListener('keydown', handleKeyDown)
75
+ return () => document.removeEventListener('keydown', handleKeyDown)
76
+ }, [isOverlayOpen, closeOverlay])
45
77
 
46
78
  const handleItemClick = useCallback((item: NavigationSidebarItem, event?: React.MouseEvent) => {
47
79
  event?.stopPropagation()
@@ -58,14 +90,12 @@ export function NavigationSidebar({ config, disabled = false }: NavigationSideba
58
90
  secondaryItems: config.items.filter(item => item.section === 'secondary'),
59
91
  }), [config.items])
60
92
 
61
- const sidebarWidth = useMemo(() => {
62
- if (isLgUp === undefined) {
63
- return `${MINIMIZED_WIDTH}px`
64
- }
65
- return isMinimized ? `${MINIMIZED_WIDTH}px` : `${EXPANDED_WIDTH}px`
66
- }, [isLgUp, isMinimized])
93
+ const sidebarWidth = useMemo(
94
+ () => (minimized ? `${MINIMIZED_WIDTH}px` : `${EXPANDED_WIDTH}px`),
95
+ [minimized],
96
+ )
67
97
 
68
- const isHydrated = isLgUp !== undefined
98
+ const isHydrated = isMdUp !== undefined && isLgUp !== undefined
69
99
 
70
100
  useLayoutEffect(() => {
71
101
  if (isHydrated && !transitionsEnabled) {
@@ -77,36 +107,47 @@ export function NavigationSidebar({ config, disabled = false }: NavigationSideba
77
107
  }, [isHydrated, transitionsEnabled])
78
108
 
79
109
  return (
80
- <aside
81
- className={cn(
82
- "flex-col h-full hidden md:flex",
83
- "bg-ods-card border-r border-ods-border",
84
- transitionsEnabled && "transition-[width] duration-300",
85
- config.className,
110
+ <>
111
+ {/* Backdrop scrim — only visible on tablet while the overlay is open */}
112
+ <div
113
+ className={cn(
114
+ "fixed inset-0 z-[40] bg-black/50",
115
+ "hidden md:block lg:hidden",
116
+ "transition-opacity duration-300",
117
+ isOverlayOpen ? "opacity-100" : "opacity-0 pointer-events-none",
118
+ )}
119
+ onClick={closeOverlay}
120
+ aria-hidden="true"
121
+ />
122
+
123
+ {/* Flex-flow placeholder — reserves the collapsed 56px slot on tablet so
124
+ the main content keeps its position while the sidebar floats above it */}
125
+ {isTablet && (
126
+ <div
127
+ className="h-full hidden md:block flex-shrink-0"
128
+ style={{ width: `${MINIMIZED_WIDTH}px` }}
129
+ aria-hidden="true"
130
+ />
86
131
  )}
87
- style={{ width: sidebarWidth }}
88
- aria-label="Main navigation sidebar"
89
- >
90
- {isHydrated && (
91
- <>
92
- <NavigationSidebarHeader minimized={isMinimized} />
93
-
94
- <div className="flex-1 flex flex-col justify-between py-4">
95
- <nav className="flex flex-col" aria-label="Primary navigation">
96
- {primaryItems.map(item => (
97
- <NavigationSidebarItemButton
98
- key={item.id}
99
- item={item}
100
- showLabel={showLabel}
101
- disabled={disabled}
102
- onClick={handleItemClick}
103
- />
104
- ))}
105
- </nav>
106
-
107
- {secondaryItems.length > 0 && (
108
- <nav className="flex flex-col" aria-label="Secondary navigation">
109
- {secondaryItems.map(item => (
132
+
133
+ <aside
134
+ className={cn(
135
+ "flex-col hidden md:flex flex-shrink-0",
136
+ "bg-ods-card border-r border-ods-border",
137
+ isTablet ? "fixed top-0 left-0 h-screen z-[45]" : "relative h-full",
138
+ transitionsEnabled && "transition-[width] duration-300",
139
+ config.className,
140
+ )}
141
+ style={{ width: sidebarWidth }}
142
+ aria-label="Main navigation sidebar"
143
+ >
144
+ {isHydrated && (
145
+ <>
146
+ <NavigationSidebarHeader minimized={minimized} />
147
+
148
+ <div className="flex-1 flex flex-col justify-between py-4 overflow-y-auto">
149
+ <nav className="flex flex-col" aria-label="Primary navigation">
150
+ {primaryItems.map(item => (
110
151
  <NavigationSidebarItemButton
111
152
  key={item.id}
112
153
  item={item}
@@ -116,18 +157,30 @@ export function NavigationSidebar({ config, disabled = false }: NavigationSideba
116
157
  />
117
158
  ))}
118
159
  </nav>
119
- )}
120
- </div>
121
160
 
122
- {isLgUp && (
161
+ {secondaryItems.length > 0 && (
162
+ <nav className="flex flex-col" aria-label="Secondary navigation">
163
+ {secondaryItems.map(item => (
164
+ <NavigationSidebarItemButton
165
+ key={item.id}
166
+ item={item}
167
+ showLabel={showLabel}
168
+ disabled={disabled}
169
+ onClick={handleItemClick}
170
+ />
171
+ ))}
172
+ </nav>
173
+ )}
174
+ </div>
175
+
123
176
  <NavigationSidebarToggle
124
- minimized={isMinimized}
177
+ minimized={minimized}
125
178
  showLabel={showLabel}
126
179
  onToggle={handleToggle}
127
180
  />
128
- )}
129
- </>
130
- )}
131
- </aside>
181
+ </>
182
+ )}
183
+ </aside>
184
+ </>
132
185
  )
133
186
  }
@@ -100,7 +100,7 @@ function InfoCell({ label, value, avatar, className }: InfoCellProps) {
100
100
 
101
101
  {/* Text content */}
102
102
  <div className="flex flex-col min-w-0">
103
- <span className="text-ods-text-primary font-['DM_Sans'] text-[14px] leading-[20px] md:text-[18px] md:leading-[24px] font-medium truncate">
103
+ <span className="text-ods-text-primary font-['DM_Sans'] text-[14px] leading-[20px] md:text-[18px] md:leading-[24px] font-medium truncate" title={value}>
104
104
  {value}
105
105
  </span>
106
106
  <span className="text-ods-text-secondary font-['DM_Sans'] text-[12px] leading-[16px] md:text-[14px] md:leading-[20px] font-medium truncate">
@@ -0,0 +1,130 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import {
5
+ ThemeProvider as NextThemesProvider,
6
+ useTheme as useNextTheme,
7
+ } from "next-themes";
8
+ import type { ThemeProviderProps as NextThemesProviderProps } from "next-themes/dist/types";
9
+
10
+ /**
11
+ * ODS theme system — thin wrapper over `next-themes`.
12
+ *
13
+ * We deliberately do NOT hand-roll a provider/no-flash script: `next-themes`
14
+ * is already a dependency, is battle-tested in Next.js (App & Pages router),
15
+ * injects its own pre-paint anti-flash script, handles SSR + localStorage +
16
+ * cross-tab sync, and also works in plain React (Vite/Tauri).
17
+ *
18
+ * Product model (locked): a MANUAL light/dark switch, default DARK,
19
+ * persisted to localStorage. No "system" mode (`enableSystem={false}`).
20
+ *
21
+ * Drives styling by setting `data-theme="light|dark"` on <html>;
22
+ * `src/styles/ods-colors.css` swaps the `--ods-*` primitives accordingly.
23
+ *
24
+ * Public API exposed by this module:
25
+ * • `<ThemeProvider>` — preconfigured next-themes provider.
26
+ * • `useTheme()` — raw next-themes hook (advanced cases).
27
+ * • `useThemeToggle()`— headless convenience hook for building toggle UI
28
+ * in consumer apps (no styled component on purpose;
29
+ * apps own their button visuals via the lib's
30
+ * existing `<Button>`).
31
+ */
32
+
33
+ export type Theme = "light" | "dark";
34
+
35
+ export const THEME_STORAGE_KEY = "ods-theme";
36
+ export const THEME_ATTRIBUTE = "data-theme";
37
+ export const DEFAULT_THEME: Theme = "dark";
38
+
39
+ export type ThemeProviderProps = Partial<NextThemesProviderProps>;
40
+
41
+ /**
42
+ * Pre-configured provider. Wrap the app once (Next.js: in the root layout;
43
+ * apps must keep `suppressHydrationWarning` on <html> — already the case).
44
+ * No `<ThemeScript>` needed — next-themes handles the no-flash script.
45
+ *
46
+ * All next-themes props are overridable, but the ODS defaults below encode
47
+ * the product decision.
48
+ */
49
+ export function ThemeProvider({ children, ...overrides }: ThemeProviderProps) {
50
+ return (
51
+ <NextThemesProvider
52
+ attribute={THEME_ATTRIBUTE}
53
+ defaultTheme={DEFAULT_THEME}
54
+ enableSystem={false}
55
+ themes={["light", "dark"]}
56
+ storageKey={THEME_STORAGE_KEY}
57
+ disableTransitionOnChange={false}
58
+ {...overrides}
59
+ >
60
+ {children}
61
+ </NextThemesProvider>
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Re-export of next-themes' `useTheme` (no custom logic on top).
67
+ * Returns `{ theme, setTheme, resolvedTheme, themes, ... }`.
68
+ */
69
+ export const useTheme = useNextTheme;
70
+
71
+ /* ------------------------------------------------------------------ */
72
+ /* useThemeToggle — headless convenience for building toggle UIs */
73
+ /* ------------------------------------------------------------------ */
74
+
75
+ export interface UseThemeToggleResult {
76
+ /** Becomes `true` after the client has hydrated and resolved the stored
77
+ * preference. Until then, `theme`/`isDark`/`isLight` reflect the SSR
78
+ * default (`DEFAULT_THEME`) — handy for rendering a stable placeholder. */
79
+ mounted: boolean;
80
+ /** Resolved active theme (`"dark"` until mounted, then real value). */
81
+ theme: Theme;
82
+ isDark: boolean;
83
+ isLight: boolean;
84
+ /** Flip dark↔light and persist. */
85
+ toggle: () => void;
86
+ /** Set explicitly to `"light"` or `"dark"` and persist. */
87
+ setTheme: (theme: Theme) => void;
88
+ }
89
+
90
+ /**
91
+ * Headless toggle helper. Build any button you like:
92
+ *
93
+ * const { isDark, toggle, mounted } = useThemeToggle()
94
+ * <Button size="icon" variant="transparent" onClick={toggle} aria-label="…">
95
+ * {mounted && (isDark ? <Sun01Icon /> : <MoonIcon />)}
96
+ * </Button>
97
+ *
98
+ * The mount gate avoids hydration mismatch (next-themes only knows the
99
+ * persisted preference on the client after mount).
100
+ */
101
+ export function useThemeToggle(): UseThemeToggleResult {
102
+ const { resolvedTheme, theme, setTheme } = useTheme();
103
+ const [mounted, setMounted] = React.useState(false);
104
+ React.useEffect(() => setMounted(true), []);
105
+
106
+ const active: Theme = mounted
107
+ ? resolvedTheme === "light" || theme === "light"
108
+ ? "light"
109
+ : "dark"
110
+ : DEFAULT_THEME;
111
+
112
+ const setOdsTheme = React.useCallback(
113
+ (next: Theme) => setTheme(next),
114
+ [setTheme],
115
+ );
116
+
117
+ const toggle = React.useCallback(
118
+ () => setTheme(active === "dark" ? "light" : "dark"),
119
+ [active, setTheme],
120
+ );
121
+
122
+ return {
123
+ mounted,
124
+ theme: active,
125
+ isDark: active === "dark",
126
+ isLight: active === "light",
127
+ toggle,
128
+ setTheme: setOdsTheme,
129
+ };
130
+ }
@@ -66,10 +66,10 @@ export function OnboardingStepCard({
66
66
  >
67
67
  {/* Left column - content */}
68
68
  <div className="flex-1 w-full md:w-auto min-w-0 flex flex-col justify-center gap-1">
69
- <h3 className="text-h4 text-ods-text-primary truncate">
69
+ <h3 className="text-h4 text-ods-text-primary truncate" title={step.title}>
70
70
  {step.title}
71
71
  </h3>
72
- <p className="font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary truncate h-[20px]">
72
+ <p className="font-['DM_Sans'] font-medium text-[14px] leading-[20px] text-ods-text-secondary truncate h-[20px]" title={step.description}>
73
73
  {step.description}
74
74
  </p>
75
75
  </div>
@@ -463,7 +463,7 @@ export function ProductReleaseCard({
463
463
  as the loaded text — zero load-to-resolve baseline shift. */}
464
464
  <span className="flex min-w-0 flex-1 flex-col gap-0.5 min-h-14">
465
465
  <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">
466
+ <span className="truncate text-sm font-semibold leading-5 text-ods-text-primary min-w-0" title={title}>
467
467
  {title}
468
468
  </span>
469
469
  {version ? (
@@ -478,7 +478,7 @@ export function ProductReleaseCard({
478
478
  </span>
479
479
  </span>
480
480
  <span className="flex items-center min-w-0 h-4">
481
- <span className="truncate text-[11px] leading-4 text-ods-text-secondary/80">
481
+ <span className="truncate text-[11px] leading-4 text-ods-text-secondary/80" title={summary || undefined}>
482
482
  {/* The literal between the curly-quote string is U+00A0
483
483
  (NBSP). The hub's `COMPACT_CARD_ROW_FILLER` is also
484
484
  NBSP; ASCII space here would let React collapse the
@@ -545,11 +545,11 @@ export function ProductReleaseCard({
545
545
  {/* Left column - content */}
546
546
  <div className="flex-1 w-full md:w-auto min-w-0 flex flex-col justify-center gap-2">
547
547
  <div className="min-h-[48px] flex items-center">
548
- <h3 className="text-h3 text-ods-text-primary tracking-[-0.36px] line-clamp-2">
548
+ <h3 className="text-h3 text-ods-text-primary tracking-[-0.36px] line-clamp-2" title={title}>
549
549
  {title}
550
550
  </h3>
551
551
  </div>
552
- <p className="text-h4 text-ods-text-secondary line-clamp-3">
552
+ <p className="text-h4 text-ods-text-secondary line-clamp-3" title={summary || ' '}>
553
553
  {summary || ' '}
554
554
  </p>
555
555
  </div>
@@ -585,11 +585,11 @@ export function ProductReleaseCard({
585
585
  {/* Left column - content */}
586
586
  <div className="flex-1 w-full md:w-auto min-w-0 flex flex-col justify-center gap-2">
587
587
  <div className="min-h-[48px] flex items-center">
588
- <h3 className="text-h3 text-ods-text-primary tracking-[-0.36px] line-clamp-2">
588
+ <h3 className="text-h3 text-ods-text-primary tracking-[-0.36px] line-clamp-2" title={title}>
589
589
  {title}
590
590
  </h3>
591
591
  </div>
592
- <p className="text-h4 text-ods-text-secondary line-clamp-3">
592
+ <p className="text-h4 text-ods-text-secondary line-clamp-3" title={summary || ' '}>
593
593
  {summary || ' '}
594
594
  </p>
595
595
  </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 &quot;{inputValue.trim()}&quot;</span>
577
+ <span className="truncate" title={`+ Create "${inputValue.trim()}"`}>+ Create &quot;{inputValue.trim()}&quot;</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 [&_svg]:h-5 [&_svg]:w-5",
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
  )}