@flamingo-stack/openframe-frontend-core 0.0.838 → 0.0.840

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 (137) hide show
  1. package/dist/build/export-map.json +1 -1
  2. package/dist/{chunk-JFR3OFE6.cjs → chunk-275AQ6LC.cjs} +6 -6
  3. package/dist/{chunk-5BM7X4GA.cjs → chunk-5GBLXY5F.cjs} +2 -2
  4. package/dist/{chunk-5L3UKDC6.cjs → chunk-6LDL3UZE.cjs} +9 -9
  5. package/dist/{chunk-SWMUW3RR.cjs → chunk-744JXTDP.cjs} +19 -19
  6. package/dist/{chunk-46LATWQR.cjs → chunk-7WKRMJI6.cjs} +5 -5
  7. package/dist/{chunk-2X5LYDNA.cjs → chunk-AT6D7BAZ.cjs} +5 -5
  8. package/dist/{chunk-C4OQIYGW.cjs → chunk-CCRXBFNL.cjs} +8 -8
  9. package/dist/{chunk-LC65AC6M.cjs → chunk-COWMXUHN.cjs} +2 -0
  10. package/dist/{chunk-5E4YEHHO.cjs → chunk-DGY5KEPY.cjs} +55 -55
  11. package/dist/{chunk-B4THEQOW.cjs → chunk-DYDQLS2R.cjs} +744 -561
  12. package/dist/{chunk-LWMWY3JO.cjs → chunk-E7M54JWW.cjs} +14 -14
  13. package/dist/{chunk-Z7A7IZWL.cjs → chunk-EV6BPF47.cjs} +236 -210
  14. package/dist/{chunk-7XRFAHJF.cjs → chunk-FHI4I3TQ.cjs} +9 -9
  15. package/dist/{chunk-ZABZIM2Y.cjs → chunk-FRL2ZN4O.cjs} +3 -3
  16. package/dist/{chunk-7Z536SPR.cjs → chunk-FW7RKWPY.cjs} +15 -1
  17. package/dist/{chunk-U4MJ2Z4V.cjs → chunk-GMK4JGC2.cjs} +46 -46
  18. package/dist/{chunk-NV5AHL4G.cjs → chunk-GUKFMMDZ.cjs} +3 -3
  19. package/dist/{chunk-JI34QKPJ.cjs → chunk-HXTZZE3R.cjs} +253 -131
  20. package/dist/{chunk-MMPMQUQM.cjs → chunk-IELPVUPR.cjs} +18 -18
  21. package/dist/{chunk-64PF3UC7.cjs → chunk-INBFKUZA.cjs} +17 -17
  22. package/dist/{chunk-APOMNC7H.cjs → chunk-IUDBG3HJ.cjs} +3 -3
  23. package/dist/{chunk-4JHO2NVI.cjs → chunk-J2IXYCEH.cjs} +26 -10
  24. package/dist/{chunk-V322TUBY.cjs → chunk-JM2XA7VY.cjs} +28 -28
  25. package/dist/{chunk-GPXKG34G.cjs → chunk-JUEAZCD6.cjs} +46 -46
  26. package/dist/{chunk-2QAUQPGC.cjs → chunk-MYAPG3FQ.cjs} +21 -21
  27. package/dist/{chunk-GRHF5SWL.cjs → chunk-NFNSPLGC.cjs} +3 -3
  28. package/dist/{chunk-4TUHHKLR.cjs → chunk-Q6WUHYOG.cjs} +72 -72
  29. package/dist/{chunk-H3WTYAVR.cjs → chunk-RSJYTQEA.cjs} +30 -30
  30. package/dist/{chunk-MG3HJIGG.cjs → chunk-UAI4VMEF.cjs} +4 -4
  31. package/dist/{chunk-S5RMRQKT.cjs → chunk-UZPYYHF5.cjs} +18 -18
  32. package/dist/{chunk-TMD55O5U.cjs → chunk-VBH2RCFM.cjs} +73 -73
  33. package/dist/{chunk-FNNDGOR3.cjs → chunk-WCK5T3UD.cjs} +43 -43
  34. package/dist/{chunk-TRZEFT2N.cjs → chunk-WI7EXAZT.cjs} +9 -5
  35. package/dist/{chunk-7QNGILIY.cjs → chunk-Z35PL3UR.cjs} +32 -32
  36. package/dist/{chunk-ENK75WN4.cjs → chunk-ZAO5SYBJ.cjs} +4 -4
  37. package/dist/{cloud-tenants-GLPDB2F5.cjs → cloud-tenants-PV63RZ73.cjs} +19 -19
  38. package/dist/components/case-studies/index.cjs +33 -33
  39. package/dist/components/chat/chat-nav-sidebar.d.ts.map +1 -1
  40. package/dist/components/chat/chat-nav-sidebar.js +17 -3
  41. package/dist/components/chat/index.cjs +13 -13
  42. package/dist/components/contact/index.cjs +20 -20
  43. package/dist/components/docs/index.cjs +23 -23
  44. package/dist/components/embeds/index.cjs +20 -20
  45. package/dist/components/faq/index.cjs +21 -21
  46. package/dist/components/features/command-box.d.ts.map +1 -1
  47. package/dist/components/features/command-box.js +4 -23
  48. package/dist/components/features/index.cjs +29 -19
  49. package/dist/components/features/software/index.d.ts +1 -0
  50. package/dist/components/features/software/index.d.ts.map +1 -1
  51. package/dist/components/features/software/index.js +12 -0
  52. package/dist/components/features/software/software-action-form.d.ts +9 -1
  53. package/dist/components/features/software/software-action-form.d.ts.map +1 -1
  54. package/dist/components/features/software/software-action-form.js +18 -1
  55. package/dist/components/features/software/update-settings-fields.d.ts +43 -0
  56. package/dist/components/features/software/update-settings-fields.d.ts.map +1 -0
  57. package/dist/components/features/software/update-settings-fields.js +202 -0
  58. package/dist/components/help-center-pages/download-apps-page.d.ts +1 -1
  59. package/dist/components/help-center-pages/download-apps-page.d.ts.map +1 -1
  60. package/dist/components/help-center-pages/download-apps-page.js +2 -3
  61. package/dist/components/help-center-pages/index.cjs +122 -123
  62. package/dist/components/index.cjs +86 -76
  63. package/dist/components/meeting-scheduler/index.cjs +82 -82
  64. package/dist/components/navigation/account-popover.d.ts.map +1 -1
  65. package/dist/components/navigation/account-popover.js +26 -2
  66. package/dist/components/navigation/app-layout-side-panel.d.ts.map +1 -1
  67. package/dist/components/navigation/app-layout-side-panel.js +2 -9
  68. package/dist/components/navigation/index.cjs +22 -22
  69. package/dist/components/navigation/navigation-sidebar-grouped.d.ts.map +1 -1
  70. package/dist/components/navigation/navigation-sidebar-grouped.js +30 -6
  71. package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
  72. package/dist/components/navigation/navigation-sidebar-item.js +14 -1
  73. package/dist/components/navigation/navigation-sidebar.d.ts.map +1 -1
  74. package/dist/components/navigation/navigation-sidebar.js +72 -3
  75. package/dist/components/onboarding-guides/index.cjs +22 -22
  76. package/dist/components/product-demo/fixtures/software-update.d.ts +2 -0
  77. package/dist/components/product-demo/fixtures/software-update.d.ts.map +1 -1
  78. package/dist/components/product-demo/fixtures/software-update.js +3 -2
  79. package/dist/components/product-demo/index.cjs +30 -30
  80. package/dist/components/related-content/index.cjs +8 -8
  81. package/dist/components/tickets/index.cjs +24 -24
  82. package/dist/components/ui/index.cjs +19 -19
  83. package/dist/components/ui/panel-resize-handle.d.ts +27 -6
  84. package/dist/components/ui/panel-resize-handle.d.ts.map +1 -1
  85. package/dist/components/ui/panel-resize-handle.js +27 -3
  86. package/dist/{devices-GHPW5WCO.cjs → devices-VECG5JNC.cjs} +20 -20
  87. package/dist/{embeddable-chat-RBC7F4TL.cjs → embeddable-chat-VRYAQQF6.cjs} +90 -90
  88. package/dist/hooks/index.cjs +11 -7
  89. package/dist/hooks/ui/index.d.ts +1 -0
  90. package/dist/hooks/ui/index.d.ts.map +1 -1
  91. package/dist/hooks/ui/index.js +1 -0
  92. package/dist/hooks/ui/use-clipped-label-tooltip.d.ts +14 -0
  93. package/dist/hooks/ui/use-clipped-label-tooltip.d.ts.map +1 -0
  94. package/dist/hooks/ui/use-clipped-label-tooltip.js +13 -0
  95. package/dist/hooks/ui/use-is-truncated.d.ts.map +1 -1
  96. package/dist/hooks/ui/use-is-truncated.js +1 -0
  97. package/dist/hooks/ui/use-media-query.d.ts.map +1 -1
  98. package/dist/hooks/ui/use-media-query.js +1 -0
  99. package/dist/hooks/ui/use-resizable-panel-size.d.ts +1 -0
  100. package/dist/hooks/ui/use-resizable-panel-size.d.ts.map +1 -1
  101. package/dist/hooks/ui/use-resizable-panel-size.js +5 -1
  102. package/dist/index.cjs +38 -24
  103. package/dist/{knowledge-4TOSY2S5.cjs → knowledge-YJSDJIXT.cjs} +20 -20
  104. package/dist/{logs-MUODFY6N.cjs → logs-42JG3NZ5.cjs} +26 -26
  105. package/dist/{policies-TJHO3VLE.cjs → policies-D4QNVJ26.cjs} +23 -23
  106. package/dist/{remote-session-CN3G7JNR.cjs → remote-session-EE6477O3.cjs} +10 -10
  107. package/dist/{scripts-X5CMYE2V.cjs → scripts-4VFYBXBE.cjs} +26 -26
  108. package/dist/{software-update-2UZKHTYM.cjs → software-update-OSDZQ5CP.cjs} +25 -25
  109. package/dist/{tickets-board-PLYD2NSY.cjs → tickets-board-44HO3LR4.cjs} +10 -10
  110. package/dist/types/navigation.d.ts +36 -0
  111. package/dist/types/navigation.d.ts.map +1 -1
  112. package/dist/utils/notifications.cjs +2 -0
  113. package/dist/utils/notifications.d.ts +1 -1
  114. package/dist/utils/notifications.d.ts.map +1 -1
  115. package/dist/utils/notifications.js +2 -0
  116. package/package.json +1 -1
  117. package/src/components/chat/chat-nav-sidebar.tsx +17 -4
  118. package/src/components/features/command-box.tsx +17 -19
  119. package/src/components/features/notifications/notification-category-icon.tsx +1 -1
  120. package/src/components/features/software/index.ts +11 -0
  121. package/src/components/features/software/software-action-form.tsx +28 -1
  122. package/src/components/features/software/update-settings-fields.tsx +265 -0
  123. package/src/components/help-center-pages/download-apps-page.tsx +4 -5
  124. package/src/components/navigation/account-popover.tsx +31 -2
  125. package/src/components/navigation/app-layout-side-panel.tsx +2 -7
  126. package/src/components/navigation/navigation-sidebar-grouped.tsx +37 -6
  127. package/src/components/navigation/navigation-sidebar-item.tsx +18 -1
  128. package/src/components/navigation/navigation-sidebar.tsx +116 -7
  129. package/src/components/product-demo/fixtures/software-update.ts +2 -2
  130. package/src/components/ui/panel-resize-handle.tsx +50 -8
  131. package/src/hooks/ui/index.ts +1 -0
  132. package/src/hooks/ui/use-clipped-label-tooltip.ts +20 -0
  133. package/src/hooks/ui/use-is-truncated.ts +2 -0
  134. package/src/hooks/ui/use-media-query.ts +2 -0
  135. package/src/hooks/ui/use-resizable-panel-size.ts +7 -1
  136. package/src/types/navigation.ts +37 -0
  137. package/src/utils/notifications.ts +2 -0
@@ -0,0 +1,265 @@
1
+ 'use client';
2
+
3
+ import { InfoCircleIcon } from '../../icons-v2-generated';
4
+ import {
5
+ Alert,
6
+ CheckboxBlock,
7
+ Label,
8
+ RadioGroupBlock,
9
+ type RadioGroupBlockOption,
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from '../../ui';
16
+
17
+ /** What an update does on a device where the app is running. */
18
+ export type SoftwareUpdateMode = 'WAIT' | 'FORCE';
19
+
20
+ /** "If the app is running": one update's behaviour on a device where the app is open. */
21
+ export interface SoftwareUpdatePolicy {
22
+ mode: SoftwareUpdateMode;
23
+ /** WAIT only: "Countdown after the deadline". Off, the update waits for the user however long it takes. */
24
+ deadlineEnabled: boolean;
25
+ /** WAIT only: seconds after the first try before the countdown starts. */
26
+ deadlineSeconds: number;
27
+ /** Seconds of countdown before the app is closed: after the deadline (WAIT) or right away (FORCE). */
28
+ countdownSeconds: number;
29
+ }
30
+
31
+ /** The Update Software form's "Update Settings": this update, and whether later releases follow it. */
32
+ export interface SoftwareUpdateSettingsValues {
33
+ policy: SoftwareUpdatePolicy;
34
+ /** "Keep it auto-updated": every new version is installed on the same devices after this update. */
35
+ autoUpdate: boolean;
36
+ /** "Same settings for auto-update": auto-updates run with `policy`; off, with `autoUpdatePolicy`. */
37
+ sameForAutoUpdate: boolean;
38
+ autoUpdatePolicy: SoftwareUpdatePolicy;
39
+ }
40
+
41
+ const DAY = 24 * 60 * 60;
42
+ const MINUTE = 60;
43
+
44
+ export const SOFTWARE_UPDATE_DEADLINE_OPTIONS: ReadonlyArray<{ seconds: number; label: string }> = [
45
+ { seconds: DAY, label: '1 day after the first try' },
46
+ { seconds: 3 * DAY, label: '3 days after the first try' },
47
+ { seconds: 7 * DAY, label: '7 days after the first try' },
48
+ { seconds: 14 * DAY, label: '14 days after the first try' },
49
+ { seconds: 30 * DAY, label: '30 days after the first try' },
50
+ ];
51
+
52
+ export const SOFTWARE_UPDATE_COUNTDOWN_OPTIONS: ReadonlyArray<{ seconds: number; label: string }> = [
53
+ { seconds: MINUTE, label: '1 minute' },
54
+ { seconds: 5 * MINUTE, label: '5 minutes' },
55
+ { seconds: 15 * MINUTE, label: '15 minutes' },
56
+ { seconds: 30 * MINUTE, label: '30 minutes' },
57
+ ];
58
+
59
+ /** Wait & notify, a countdown 7 days after the first try, 5 minutes long: what the server applies when sent none. */
60
+ export const DEFAULT_SOFTWARE_UPDATE_POLICY: SoftwareUpdatePolicy = {
61
+ mode: 'WAIT',
62
+ deadlineEnabled: true,
63
+ deadlineSeconds: 7 * DAY,
64
+ countdownSeconds: 5 * MINUTE,
65
+ };
66
+
67
+ export const DEFAULT_SOFTWARE_UPDATE_SETTINGS: SoftwareUpdateSettingsValues = {
68
+ policy: DEFAULT_SOFTWARE_UPDATE_POLICY,
69
+ autoUpdate: true,
70
+ sameForAutoUpdate: true,
71
+ autoUpdatePolicy: DEFAULT_SOFTWARE_UPDATE_POLICY,
72
+ };
73
+
74
+ const MODE_OPTIONS: RadioGroupBlockOption[] = [
75
+ {
76
+ value: 'WAIT',
77
+ label: 'Wait & notify',
78
+ description: 'The user gets Update now or Ask Later. If they close the app, it updates then.',
79
+ },
80
+ {
81
+ value: 'FORCE',
82
+ label: 'Force Update',
83
+ description: 'The user gets a short countdown, then the app closes and updates.',
84
+ },
85
+ ];
86
+
87
+ const SAME_SETTINGS_DESCRIPTION: Record<SoftwareUpdateMode, string> = {
88
+ WAIT: 'Auto-updates follow "If the app is running" above. Uncheck to set them separately.',
89
+ FORCE: 'Later releases will also be force-updated.',
90
+ };
91
+
92
+ const groupLabelClass = 'text-h5 text-ods-text-secondary';
93
+
94
+ function SecondsSelect({
95
+ label,
96
+ value,
97
+ options,
98
+ onChange,
99
+ }: {
100
+ label: string;
101
+ value: number;
102
+ options: ReadonlyArray<{ seconds: number; label: string }>;
103
+ onChange: (seconds: number) => void;
104
+ }) {
105
+ return (
106
+ <div className="flex min-w-0 flex-col gap-[var(--spacing-system-xxs)]">
107
+ <Label className="text-h4">{label}</Label>
108
+ <Select value={String(value)} onValueChange={next => onChange(Number(next))}>
109
+ <SelectTrigger aria-label={label}>
110
+ <SelectValue />
111
+ </SelectTrigger>
112
+ <SelectContent>
113
+ {options.map(option => (
114
+ <SelectItem key={option.seconds} value={String(option.seconds)}>
115
+ {option.label}
116
+ </SelectItem>
117
+ ))}
118
+ </SelectContent>
119
+ </Select>
120
+ </div>
121
+ );
122
+ }
123
+
124
+ /** "If the app is running" with its deadline and countdown. */
125
+ function UpdatePolicyFields({
126
+ name,
127
+ label,
128
+ policy,
129
+ onChange,
130
+ }: {
131
+ /** The radio group's name: two policies on one page must not share it. */
132
+ name: string;
133
+ label: string;
134
+ policy: SoftwareUpdatePolicy;
135
+ onChange: (policy: SoftwareUpdatePolicy) => void;
136
+ }) {
137
+ const waits = policy.mode === 'WAIT';
138
+ const deadline = waits && policy.deadlineEnabled;
139
+ const countdown = (
140
+ <SecondsSelect
141
+ label="Countdown"
142
+ value={policy.countdownSeconds}
143
+ options={SOFTWARE_UPDATE_COUNTDOWN_OPTIONS}
144
+ onChange={countdownSeconds => onChange({ ...policy, countdownSeconds })}
145
+ />
146
+ );
147
+ return (
148
+ <div className="flex flex-col gap-[var(--spacing-system-m)]">
149
+ <div className="flex flex-col gap-[var(--spacing-system-xxs)]">
150
+ <span className={groupLabelClass}>{label}</span>
151
+ <RadioGroupBlock
152
+ name={name}
153
+ aria-label={label}
154
+ variant="grouped"
155
+ value={policy.mode}
156
+ onValueChange={mode => onChange({ ...policy, mode: mode as SoftwareUpdateMode })}
157
+ options={MODE_OPTIONS}
158
+ itemClassName="py-[var(--spacing-system-sf)]"
159
+ />
160
+ </div>
161
+ {waits ? (
162
+ <div className="flex flex-col gap-[var(--spacing-system-xsf)]">
163
+ <CheckboxBlock
164
+ id={`${name}-deadline`}
165
+ checked={policy.deadlineEnabled}
166
+ onCheckedChange={deadlineEnabled => onChange({ ...policy, deadlineEnabled })}
167
+ label="Countdown after the deadline"
168
+ description="If the update still isn’t installed by the deadline, the user gets a countdown and then it installs."
169
+ />
170
+ {deadline && (
171
+ <div className="grid grid-cols-1 gap-[var(--spacing-system-lf)] content-md:grid-cols-3 content-md:items-start">
172
+ <SecondsSelect
173
+ label="Deadline"
174
+ value={policy.deadlineSeconds}
175
+ options={SOFTWARE_UPDATE_DEADLINE_OPTIONS}
176
+ onChange={deadlineSeconds => onChange({ ...policy, deadlineSeconds })}
177
+ />
178
+ {countdown}
179
+ </div>
180
+ )}
181
+ </div>
182
+ ) : (
183
+ <div className="grid grid-cols-1 gap-[var(--spacing-system-lf)] content-md:grid-cols-3">{countdown}</div>
184
+ )}
185
+ </div>
186
+ );
187
+ }
188
+
189
+ export interface SoftwareUpdateSettingsFieldsProps {
190
+ value: SoftwareUpdateSettingsValues;
191
+ onChange: (value: SoftwareUpdateSettingsValues) => void;
192
+ }
193
+
194
+ /**
195
+ * Update Software's "Update Settings" (design 988:21462, the separate auto-update
196
+ * rules 1034:8173): what happens on a device where the app is running, and
197
+ * whether later releases are installed the same way.
198
+ */
199
+ export function SoftwareUpdateSettingsFields({ value, onChange }: SoftwareUpdateSettingsFieldsProps) {
200
+ return (
201
+ <section aria-labelledby="software-update-settings" className="flex flex-col gap-[var(--spacing-system-l)]">
202
+ <h2 id="software-update-settings" className="pt-[var(--spacing-system-l)] text-ods-text-primary text-h2">
203
+ Update Settings
204
+ </h2>
205
+
206
+ <UpdatePolicyFields
207
+ name="updatePolicy"
208
+ label="If the app is running"
209
+ policy={value.policy}
210
+ onChange={policy => onChange({ ...value, policy })}
211
+ />
212
+
213
+ <div className="flex flex-col gap-[var(--spacing-system-xxs)]">
214
+ <span className={groupLabelClass}>Auto-update</span>
215
+ <div className="flex flex-col gap-[var(--spacing-system-sf)]">
216
+ <CheckboxBlock
217
+ id="software-auto-update"
218
+ checked={value.autoUpdate}
219
+ onCheckedChange={autoUpdate => onChange({ ...value, autoUpdate })}
220
+ label="Keep it auto-updated"
221
+ description="After this update, OpenFrame installs each new version from Winget or Brew on the selected devices. When off, only this update runs."
222
+ />
223
+ {value.autoUpdate && (
224
+ <>
225
+ <CheckboxBlock
226
+ id="software-auto-update-same"
227
+ checked={value.sameForAutoUpdate}
228
+ onCheckedChange={sameForAutoUpdate => onChange({ ...value, sameForAutoUpdate })}
229
+ label="Same settings for auto-update"
230
+ description={SAME_SETTINGS_DESCRIPTION[value.policy.mode]}
231
+ />
232
+ {!value.sameForAutoUpdate && (
233
+ <div className="rounded-[6px] border border-ods-border p-[var(--spacing-system-m)]">
234
+ <UpdatePolicyFields
235
+ name="autoUpdatePolicy"
236
+ label="For auto-updates"
237
+ policy={value.autoUpdatePolicy}
238
+ onChange={autoUpdatePolicy => onChange({ ...value, autoUpdatePolicy })}
239
+ />
240
+ </div>
241
+ )}
242
+ </>
243
+ )}
244
+ </div>
245
+ </div>
246
+
247
+ {/* Core Alert on the card surface, as a note: it is there from the start, nothing to announce. The icon sits in
248
+ a span so Alert's `[&>svg]` positioning does not apply. */}
249
+ <Alert
250
+ role="note"
251
+ className="flex items-center gap-[var(--spacing-system-m)] rounded-[6px] border-ods-border bg-ods-card p-[var(--spacing-system-m)]"
252
+ >
253
+ <span className="shrink-0 text-ods-text-secondary">
254
+ <InfoCircleIcon size={24} color="currentColor" />
255
+ </span>
256
+ <div className="flex min-w-0 flex-1 flex-col gap-[var(--spacing-system-xxs)]">
257
+ <p className="text-ods-text-primary text-h3">Apps installed by hand get reinstalled</p>
258
+ <p className="text-ods-text-secondary text-h6">
259
+ On the first update, OpenFrame reinstalls them with Winget or Brew so it can keep them updated.
260
+ </p>
261
+ </div>
262
+ </Alert>
263
+ </section>
264
+ );
265
+ }
@@ -7,7 +7,7 @@
7
7
  * lists its platforms; it does not hide them behind tabs, so nothing moves
8
8
  * when a visitor looks for another system). In a card the installer is THE
9
9
  * action (the other architecture is a quieter button) and each package
10
- * manager is one line under it, with its copy button.
10
+ * manager is one row under it, with its copy button.
11
11
  * - The mobile app: the store badges, and the install QR code beside them. The
12
12
  * code and the address it encodes are the server's (`mobile.install`): the
13
13
  * website's install link, which the server answers with a redirect to the
@@ -111,7 +111,7 @@ export interface DownloadAppsWebOption {
111
111
 
112
112
  const SECTIONS_CLASS = 'flex flex-col gap-[var(--spacing-system-xlf)]';
113
113
  const SECTION_CLASS = 'flex flex-col gap-[var(--spacing-system-lf)]';
114
- const CARDS_GRID_CLASS = 'grid grid-cols-1 gap-[var(--spacing-system-lf)] content-md:grid-cols-2';
114
+ const CARDS_GRID_CLASS = 'grid grid-cols-1 gap-[var(--spacing-system-lf)] content-lg:grid-cols-2';
115
115
  /** One card takes the row; two or more share it. */
116
116
  const cardsGridClass = (cards: number) => (cards > 1 ? CARDS_GRID_CLASS : 'grid grid-cols-1');
117
117
  const CARD_CLASS =
@@ -183,7 +183,7 @@ function installerLabel(download: AppDownload, siblings: number): string {
183
183
  return siblings > 1 && architecture ? `${label} (${architecture})` : label;
184
184
  }
185
185
 
186
- /** One system's card: its logo and name, its installers side by side, then each package manager as one line. */
186
+ /** One system's card: its logo and name, its installers side by side, then each package manager as one row. */
187
187
  function SystemCard({
188
188
  system,
189
189
  onDownloadAction,
@@ -243,8 +243,7 @@ function SystemCard({
243
243
  key={row.id}
244
244
  title={row.label ?? undefined}
245
245
  command={row.command}
246
- maxLines={1}
247
- commandClassName="text-ods-accent"
246
+ commandClassName="break-words text-ods-accent"
248
247
  copyAriaLabel={`Copy the ${row.label ?? 'install'} command`}
249
248
  onCopy={() => {
250
249
  void copy(row.command as string);
@@ -2,9 +2,11 @@
2
2
 
3
3
  import { useControllableState } from '@radix-ui/react-use-controllable-state';
4
4
  import type { ReactNode } from 'react';
5
+ import { useClippedLabelTooltip } from '../../hooks/ui/use-clipped-label-tooltip';
5
6
  import { cn } from '../../utils';
6
7
  import { Chevron02DownIcon } from '../icons-v2-generated';
7
8
  import { ActionsMenuDropdown, type ActionsMenuGroup, type ActionsMenuItem } from '../ui/actions-menu';
9
+ import { FloatingTooltip } from '../ui/floating-tooltip';
8
10
  import { SquareAvatar } from '../ui/square-avatar';
9
11
  import { UnreadCountBadge, UnreadDot } from './unread-dot';
10
12
 
@@ -124,6 +126,11 @@ export function AccountPopover({
124
126
  className,
125
127
  }: AccountPopoverProps) {
126
128
  const unread = items.some(entry => entry.type !== 'divider' && (entry.count ?? 0) > 0);
129
+ // The name and subtitle a narrow sidebar clips: their full text in a tooltip
130
+ // over them, only while clipped and where a pointer can hover. The rail's
131
+ // tooltip is the trigger's `title`.
132
+ const { ref: nameRef, enabled: nameTooltip } = useClippedLabelTooltip<HTMLSpanElement>(name);
133
+ const { ref: subtitleRef, enabled: subtitleTooltip } = useClippedLabelTooltip<HTMLSpanElement>(subtitle);
127
134
  const [open = false, setOpen] = useControllableState({
128
135
  prop: openProp,
129
136
  defaultProp: false,
@@ -182,14 +189,36 @@ export function AccountPopover({
182
189
  {/* The chevron follows the name, so it reads as this row's menu and not
183
190
  as one more of the buttons a host sets at the row's end. */}
184
191
  <span className="flex min-w-0 items-center gap-[var(--spacing-system-xxs)]">
185
- <span className="truncate text-ods-text-primary text-h4">{name}</span>
192
+ <FloatingTooltip
193
+ content={name}
194
+ side="top"
195
+ as="span"
196
+ disabled={collapsed || !nameTooltip}
197
+ triggerClassName="min-w-0"
198
+ >
199
+ <span ref={nameRef} className="block truncate text-ods-text-primary text-h4">
200
+ {name}
201
+ </span>
202
+ </FloatingTooltip>
186
203
  <Chevron02DownIcon
187
204
  size={16}
188
205
  className="shrink-0 text-ods-text-secondary transition-transform duration-200 group-data-[state=open]/account:rotate-180"
189
206
  aria-hidden="true"
190
207
  />
191
208
  </span>
192
- {subtitle && <span className="truncate text-ods-text-secondary text-code">{subtitle}</span>}
209
+ {subtitle && (
210
+ <FloatingTooltip
211
+ content={subtitle}
212
+ side="top"
213
+ as="span"
214
+ disabled={collapsed || !subtitleTooltip}
215
+ triggerClassName="min-w-0"
216
+ >
217
+ <span ref={subtitleRef} className="block truncate text-ods-text-secondary text-code">
218
+ {subtitle}
219
+ </span>
220
+ </FloatingTooltip>
221
+ )}
193
222
  </span>
194
223
  </button>
195
224
  }
@@ -13,7 +13,7 @@ import {
13
13
  import { useIsomorphicLayoutEffect } from '../../hooks/ui/use-isomorphic-layout-effect';
14
14
  import { breakpoints, useMediaQuery } from '../../hooks/ui/use-media-query';
15
15
  import { cn } from '../../utils/cn';
16
- import { Menu01Icon } from '../icons-v2-generated';
16
+ import { PanelResizeGrip } from '../ui/panel-resize-handle';
17
17
 
18
18
  /**
19
19
  * - `docked`: a column beside the content, resizable.
@@ -523,12 +523,7 @@ function SidePanelResizeHandle({ state, label, controls }: SidePanelResizeHandle
523
523
  onKeyDown={handleKeyDown}
524
524
  className="group absolute inset-y-0 left-0 z-10 flex w-4 -translate-x-1/2 cursor-col-resize touch-none select-none items-center justify-center outline-none"
525
525
  >
526
- <span
527
- aria-hidden
528
- className="flex h-8 w-4 items-center justify-center rounded-sm border border-ods-border bg-ods-card text-ods-text-secondary transition-colors group-hover:border-ods-border-hover group-focus-visible:border-ods-accent group-focus-visible:text-ods-accent"
529
- >
530
- <Menu01Icon size={12} />
531
- </span>
526
+ <PanelResizeGrip />
532
527
  </div>
533
528
  );
534
529
  }
@@ -2,9 +2,11 @@
2
2
 
3
3
  import { cloneElement, isValidElement, type MouseEvent, type ReactNode, useId, useState } from 'react';
4
4
  import Link from '../../embed-shims/next-link';
5
+ import { useClippedLabelTooltip } from '../../hooks/ui/use-clipped-label-tooltip';
5
6
  import type { NavigationSidebarItem } from '../../types/navigation';
6
7
  import { cn } from '../../utils';
7
8
  import { UnifiedSkeleton } from '../loading/unified-skeleton';
9
+ import { FloatingTooltip } from '../ui/floating-tooltip';
8
10
  import { NavigationItemBadge } from './navigation-item-badge';
9
11
  import { UnreadDot } from './unread-dot';
10
12
 
@@ -150,8 +152,15 @@ function RowEnd({ count }: { count: number }) {
150
152
  return count > 0 ? <CountPill count={count} className="ml-auto" /> : null;
151
153
  }
152
154
 
153
- function RowLabel({ item }: { item: NavigationSidebarItem }) {
155
+ /**
156
+ * The row's label column. A label the expanded sidebar clips (a narrow width,
157
+ * a long label) shows its full text in a tooltip over it, as `TruncateText`
158
+ * offers it — only while it IS clipped, and only where a pointer can hover.
159
+ * The rail (`showLabel` false) has its own: the row's `title`.
160
+ */
161
+ function RowLabel({ item, showLabel = true }: { item: NavigationSidebarItem; showLabel?: boolean }) {
154
162
  const unreadCount = item.unreadCount ?? 0;
163
+ const { ref: labelRef, enabled: labelTooltip } = useClippedLabelTooltip<HTMLSpanElement>(item.label);
155
164
  return (
156
165
  <span
157
166
  className={cn(
@@ -159,7 +168,17 @@ function RowLabel({ item }: { item: NavigationSidebarItem }) {
159
168
  EXPANDED_ONLY,
160
169
  )}
161
170
  >
162
- <span className="min-w-0 truncate text-h4">{item.label}</span>
171
+ <FloatingTooltip
172
+ content={item.label}
173
+ side="top"
174
+ as="span"
175
+ disabled={!showLabel || !labelTooltip}
176
+ triggerClassName="min-w-0"
177
+ >
178
+ <span ref={labelRef} className="block min-w-0 truncate text-h4">
179
+ {item.label}
180
+ </span>
181
+ </FloatingTooltip>
163
182
  {item.badge && <NavigationItemBadge label={item.badge} compact />}
164
183
  <RowEnd count={unreadCount} />
165
184
  </span>
@@ -204,7 +223,7 @@ function GroupedLeafRow({
204
223
  const content = (
205
224
  <>
206
225
  <RowIcon item={item} size={20} className={lit ? 'text-ods-accent' : idleIcon('row')} />
207
- <RowLabel item={item} />
226
+ <RowLabel item={item} showLabel={showLabel} />
208
227
  </>
209
228
  );
210
229
  const shared = {
@@ -248,6 +267,8 @@ function GroupedChildRow({
248
267
  }) {
249
268
  const name = accessibleName(item);
250
269
  const lit = isActive && !disabled;
270
+ // The clipped label's tooltip, as `RowLabel` offers it.
271
+ const { ref: labelRef, enabled: labelTooltip } = useClippedLabelTooltip<HTMLSpanElement>(item.label);
251
272
  const className = cn(
252
273
  // The icon column flush, under the parent's icon at both widths.
253
274
  'group/nav-child relative flex h-11 w-full items-stretch gap-[var(--spacing-system-xxs)] rounded-md text-left',
@@ -276,7 +297,17 @@ function GroupedChildRow({
276
297
  !lit && !disabled && isPending && 'after:opacity-100',
277
298
  )}
278
299
  >
279
- <span className="min-w-0 truncate text-h4">{item.label}</span>
300
+ <FloatingTooltip
301
+ content={item.label}
302
+ side="top"
303
+ as="span"
304
+ disabled={!showLabel || !labelTooltip}
305
+ triggerClassName="min-w-0"
306
+ >
307
+ <span ref={labelRef} className="block min-w-0 truncate text-h4">
308
+ {item.label}
309
+ </span>
310
+ </FloatingTooltip>
280
311
  {item.badge && <NavigationItemBadge label={item.badge} compact />}
281
312
  <RowEnd count={item.unreadCount ?? 0} />
282
313
  </span>
@@ -365,7 +396,7 @@ function GroupedGroup({
365
396
  )}
366
397
  >
367
398
  <RowIcon item={item} size={20} className={lit ? 'text-ods-accent' : idleIcon('row')} />
368
- <RowLabel item={item} />
399
+ <RowLabel item={item} showLabel={showLabel} />
369
400
  </button>
370
401
  <div
371
402
  id={groupDomId}
@@ -517,7 +548,7 @@ export function NavigationSidebarActionRow({
517
548
  const content = (
518
549
  <>
519
550
  <RowIcon item={entry} size={20} className={lit || (outline && !disabled) ? 'text-ods-accent' : idleIcon('row')} />
520
- <RowLabel item={entry} />
551
+ <RowLabel item={entry} showLabel={!minimized} />
521
552
  </>
522
553
  );
523
554
  const shared = { className: classes, title: minimized ? name : undefined, 'aria-label': name };
@@ -2,10 +2,12 @@
2
2
 
3
3
  import { cloneElement, memo, type ReactElement, type ReactNode, type MouseEvent } from 'react';
4
4
  import Link from '../../embed-shims/next-link';
5
+ import { useClippedLabelTooltip } from '../../hooks/ui/use-clipped-label-tooltip';
5
6
  import type { NavigationSidebarItem } from '../../types/navigation';
6
7
  import { cn } from '../../utils';
7
8
  import { UnifiedSkeleton } from '../loading/unified-skeleton';
8
9
  import { BUTTON_KIND_CLASS, Button } from '../ui/button';
10
+ import { FloatingTooltip } from '../ui/floating-tooltip';
9
11
  import { useLoadingScope } from '../ui/loading-scope';
10
12
  import { NavigationItemBadge } from './navigation-item-badge';
11
13
  import { UnreadDot } from './unread-dot';
@@ -76,6 +78,11 @@ function LoadedNavigationSidebarItemButton({
76
78
  // The accessible name carries the stamp — "Devices (Beta)" — because `aria-label`
77
79
  // replaces the content; the rail's tooltip is the same string.
78
80
  const name = item.badge ? `${item.label} (${item.badge})` : item.label;
81
+ // A label the expanded sidebar clips (a narrow width, a long label): its full
82
+ // text in a tooltip over it, as `TruncateText` offers it — only while it IS
83
+ // clipped, since a label that fits would only repeat itself, and only where a
84
+ // pointer can hover. The rail's tooltip is the row's `title` below.
85
+ const { ref: labelRef, enabled: labelTooltip } = useClippedLabelTooltip<HTMLSpanElement>(item.label);
79
86
 
80
87
  const content: ReactNode = (
81
88
  <>
@@ -98,7 +105,17 @@ function LoadedNavigationSidebarItemButton({
98
105
  )}
99
106
  aria-hidden={!showLabel}
100
107
  >
101
- <span className="min-w-0 truncate text-h4">{item.label}</span>
108
+ <FloatingTooltip
109
+ content={item.label}
110
+ side="top"
111
+ as="span"
112
+ disabled={!showLabel || !labelTooltip}
113
+ triggerClassName="min-w-0"
114
+ >
115
+ <span ref={labelRef} className="block min-w-0 truncate text-h4">
116
+ {item.label}
117
+ </span>
118
+ </FloatingTooltip>
102
119
  {item.badge && <NavigationItemBadge label={item.badge} className="ml-[var(--spacing-system-xs)] shrink-0" />}
103
120
  </span>
104
121