@flamingo-stack/openframe-frontend-core 0.0.277 → 0.0.278

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 (102) hide show
  1. package/dist/{chunk-LDEXTWMK.js → chunk-5IJ46KAV.js} +2 -1
  2. package/dist/{chunk-MHQPAA3W.js → chunk-5NL62LIF.js} +4 -4
  3. package/dist/{chunk-KBKW4LVO.js → chunk-737ZLQ2T.js} +3 -3
  4. package/dist/{chunk-LALAS7OH.cjs → chunk-7LTWNUHB.cjs} +1465 -1115
  5. package/dist/chunk-7LTWNUHB.cjs.map +1 -0
  6. package/dist/{chunk-2GLHJ7ZY.cjs → chunk-7XUBAK5K.cjs} +12 -12
  7. package/dist/{chunk-2GLHJ7ZY.cjs.map → chunk-7XUBAK5K.cjs.map} +1 -1
  8. package/dist/{chunk-L5P4IFPK.cjs → chunk-BBDJCXHW.cjs} +10 -10
  9. package/dist/{chunk-L5P4IFPK.cjs.map → chunk-BBDJCXHW.cjs.map} +1 -1
  10. package/dist/{chunk-NCJENC5G.cjs → chunk-DO55GWNT.cjs} +19 -19
  11. package/dist/{chunk-NCJENC5G.cjs.map → chunk-DO55GWNT.cjs.map} +1 -1
  12. package/dist/{chunk-P6Y3S6QK.js → chunk-GO45CXT5.js} +2239 -1889
  13. package/dist/chunk-GO45CXT5.js.map +1 -0
  14. package/dist/{chunk-LH7R56RR.cjs → chunk-HR4CVAVH.cjs} +20 -18
  15. package/dist/chunk-HR4CVAVH.cjs.map +1 -0
  16. package/dist/{chunk-ZSPPV2SX.cjs → chunk-L4BJU5CE.cjs} +38 -38
  17. package/dist/{chunk-ZSPPV2SX.cjs.map → chunk-L4BJU5CE.cjs.map} +1 -1
  18. package/dist/{chunk-DMAPXENZ.js → chunk-MDHNLZME.js} +4 -4
  19. package/dist/{chunk-6A5GGUIA.js → chunk-MVUN35FH.js} +3 -3
  20. package/dist/{chunk-UCX75VEM.js → chunk-R6SHY6ZQ.js} +2 -2
  21. package/dist/{chunk-ZS4XNIZK.js → chunk-RGCVYGA2.js} +5 -3
  22. package/dist/chunk-RGCVYGA2.js.map +1 -0
  23. package/dist/{chunk-27NKVNWD.cjs → chunk-WYQ5VFXA.cjs} +29 -29
  24. package/dist/{chunk-27NKVNWD.cjs.map → chunk-WYQ5VFXA.cjs.map} +1 -1
  25. package/dist/{chunk-N5UOJC3V.cjs → chunk-XXI7BNB6.cjs} +3 -2
  26. package/dist/chunk-XXI7BNB6.cjs.map +1 -0
  27. package/dist/components/chat/index.cjs +4 -4
  28. package/dist/components/chat/index.js +3 -3
  29. package/dist/components/contact/index.cjs +5 -5
  30. package/dist/components/contact/index.js +4 -4
  31. package/dist/components/faq/index.cjs +5 -5
  32. package/dist/components/faq/index.js +4 -4
  33. package/dist/components/features/index.cjs +16 -4
  34. package/dist/components/features/index.cjs.map +1 -1
  35. package/dist/components/features/index.d.ts +1 -0
  36. package/dist/components/features/index.d.ts.map +1 -1
  37. package/dist/components/features/index.js +16 -4
  38. package/dist/components/features/time-tracker/index.d.ts +10 -0
  39. package/dist/components/features/time-tracker/index.d.ts.map +1 -0
  40. package/dist/components/features/time-tracker/time-tracker-context.d.ts +17 -0
  41. package/dist/components/features/time-tracker/time-tracker-context.d.ts.map +1 -0
  42. package/dist/components/features/time-tracker/time-tracker-header-button.d.ts +13 -0
  43. package/dist/components/features/time-tracker/time-tracker-header-button.d.ts.map +1 -0
  44. package/dist/components/features/time-tracker/time-tracker-panel.d.ts +7 -0
  45. package/dist/components/features/time-tracker/time-tracker-panel.d.ts.map +1 -0
  46. package/dist/components/features/time-tracker/types.d.ts +54 -0
  47. package/dist/components/features/time-tracker/types.d.ts.map +1 -0
  48. package/dist/components/features/time-tracker/use-tracker-clock.d.ts +14 -0
  49. package/dist/components/features/time-tracker/use-tracker-clock.d.ts.map +1 -0
  50. package/dist/components/index.cjs +114 -102
  51. package/dist/components/index.cjs.map +1 -1
  52. package/dist/components/index.js +19 -7
  53. package/dist/components/index.js.map +1 -1
  54. package/dist/components/navigation/app-header.d.ts +2 -0
  55. package/dist/components/navigation/app-header.d.ts.map +1 -1
  56. package/dist/components/navigation/index.cjs +4 -4
  57. package/dist/components/navigation/index.js +3 -3
  58. package/dist/components/onboarding-guides/index.cjs +23 -23
  59. package/dist/components/onboarding-guides/index.js +3 -3
  60. package/dist/components/related-content/index.cjs +5 -5
  61. package/dist/components/related-content/index.js +4 -4
  62. package/dist/components/tickets/index.cjs +72 -72
  63. package/dist/components/tickets/index.js +5 -5
  64. package/dist/components/ui/autocomplete.d.ts.map +1 -1
  65. package/dist/components/ui/file-manager/index.cjs +20 -20
  66. package/dist/components/ui/file-manager/index.js +1 -1
  67. package/dist/components/ui/hidden-tags-popup.d.ts.map +1 -1
  68. package/dist/components/ui/index.cjs +4 -4
  69. package/dist/components/ui/index.js +3 -3
  70. package/dist/index.cjs +24 -4
  71. package/dist/index.cjs.map +1 -1
  72. package/dist/index.js +23 -3
  73. package/dist/utils/index.cjs +37 -4
  74. package/dist/utils/index.cjs.map +1 -1
  75. package/dist/utils/index.d.ts +1 -1
  76. package/dist/utils/index.d.ts.map +1 -1
  77. package/dist/utils/index.js +34 -5
  78. package/dist/utils/index.js.map +1 -1
  79. package/package.json +1 -1
  80. package/src/components/features/index.ts +1 -0
  81. package/src/components/features/time-tracker/index.ts +18 -0
  82. package/src/components/features/time-tracker/time-tracker-context.tsx +45 -0
  83. package/src/components/features/time-tracker/time-tracker-header-button.tsx +87 -0
  84. package/src/components/features/time-tracker/time-tracker-panel.tsx +270 -0
  85. package/src/components/features/time-tracker/types.ts +61 -0
  86. package/src/components/features/time-tracker/use-tracker-clock.ts +39 -0
  87. package/src/components/navigation/app-header.tsx +9 -0
  88. package/src/components/ui/autocomplete.tsx +2 -7
  89. package/src/components/ui/hidden-tags-popup.tsx +3 -1
  90. package/src/stories/TimeTracker.stories.tsx +355 -0
  91. package/src/utils/index.ts +13 -1
  92. package/dist/chunk-LALAS7OH.cjs.map +0 -1
  93. package/dist/chunk-LH7R56RR.cjs.map +0 -1
  94. package/dist/chunk-N5UOJC3V.cjs.map +0 -1
  95. package/dist/chunk-P6Y3S6QK.js.map +0 -1
  96. package/dist/chunk-ZS4XNIZK.js.map +0 -1
  97. /package/dist/{chunk-LDEXTWMK.js.map → chunk-5IJ46KAV.js.map} +0 -0
  98. /package/dist/{chunk-MHQPAA3W.js.map → chunk-5NL62LIF.js.map} +0 -0
  99. /package/dist/{chunk-KBKW4LVO.js.map → chunk-737ZLQ2T.js.map} +0 -0
  100. /package/dist/{chunk-DMAPXENZ.js.map → chunk-MDHNLZME.js.map} +0 -0
  101. /package/dist/{chunk-6A5GGUIA.js.map → chunk-MVUN35FH.js.map} +0 -0
  102. /package/dist/{chunk-UCX75VEM.js.map → chunk-R6SHY6ZQ.js.map} +0 -0
@@ -0,0 +1,355 @@
1
+ import type { Meta, StoryObj } from '@storybook/nextjs-vite'
2
+ import * as React from 'react'
3
+ import {
4
+ TimeTrackerHeaderButton,
5
+ TimeTrackerPanel,
6
+ TimeTrackerProvider,
7
+ type TimeTrackerData,
8
+ type TimeTrackerEntry,
9
+ type TimeTrackerStatus,
10
+ } from '../components/features/time-tracker'
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Mock data + a stand-in "host" that simulates the openframe-frontend backend
14
+ // (timer transitions, ticket search, persisting finished entries).
15
+ // ---------------------------------------------------------------------------
16
+
17
+ const ALL_TICKETS = [
18
+ { id: 'TICK-101', label: '#101 · Printer not working' },
19
+ { id: 'TICK-102', label: '#102 · VPN connection failed' },
20
+ { id: 'TICK-103', label: '#103 · Outlook keeps crashing' },
21
+ { id: 'TICK-104', label: '#104 · Disk Cleanup Assistance' },
22
+ { id: 'TICK-105', label: '#105 · Wi-Fi dropping intermittently' },
23
+ { id: 'TICK-106', label: '#106 · New employee laptop setup' },
24
+ { id: 'TICK-107', label: '#107 · Two-factor authentication locked out' },
25
+ ]
26
+
27
+ const SEED_ENTRIES: TimeTrackerEntry[] = [
28
+ {
29
+ id: 'e1',
30
+ durationLabel: '00:08:17',
31
+ dateLabel: '07/12/25',
32
+ title: 'Disk Cleanup Assistance',
33
+ description: 'Walked user through Storage Sense setup, scheduled monthly cleanup',
34
+ },
35
+ {
36
+ id: 'e2',
37
+ durationLabel: '00:23:04',
38
+ dateLabel: '07/11/25',
39
+ title: 'VPN connection failed',
40
+ description: 'Reissued client certificate and verified split-tunnel routes',
41
+ },
42
+ {
43
+ id: 'e3',
44
+ durationLabel: '01:12:49',
45
+ dateLabel: '07/10/25',
46
+ title: 'New employee laptop setup',
47
+ description: 'Imaged device, joined domain, installed standard software bundle',
48
+ },
49
+ ]
50
+
51
+ function pad(n: number) {
52
+ return String(n).padStart(2, '0')
53
+ }
54
+
55
+ function formatDuration(ms: number) {
56
+ const total = Math.max(0, Math.floor(ms / 1000))
57
+ return `${pad(Math.floor(total / 3600))}:${pad(Math.floor((total % 3600) / 60))}:${pad(total % 60)}`
58
+ }
59
+
60
+ function formatDate(d: Date) {
61
+ return `${pad(d.getMonth() + 1)}/${pad(d.getDate())}/${String(d.getFullYear()).slice(-2)}`
62
+ }
63
+
64
+ const meta = {
65
+ title: 'Features/TimeTracker',
66
+ component: TimeTrackerPanel,
67
+ parameters: {
68
+ layout: 'padded',
69
+ docs: {
70
+ description: {
71
+ component:
72
+ 'Purely-UI time tracker. The host wraps the app in `<TimeTrackerProvider>` (supplying `status`, `runningSince`, ticket options, last entries, and the start/pause/resume/submit callbacks) and passes `showTimeTracker` to `AppHeader`. The header button shows the live elapsed time while a session is active and opens the popup below it. The timer is display-only — it ticks from the host-provided `runningSince`; the backend owns the real start time.',
73
+ },
74
+ },
75
+ },
76
+ // Default args satisfy the panel's required props for autodocs; every story
77
+ // below overrides them with an interactive `render`.
78
+ args: {
79
+ status: 'ready',
80
+ ticketOptions: ALL_TICKETS,
81
+ selectedTicketIds: [],
82
+ onSelectedTicketsChange: () => {},
83
+ notes: '',
84
+ onNotesChange: () => {},
85
+ lastEntries: SEED_ENTRIES,
86
+ onStart: () => {},
87
+ onPause: () => {},
88
+ onResume: () => {},
89
+ onCancel: () => {},
90
+ onSubmit: () => {},
91
+ onClose: () => {},
92
+ },
93
+ } satisfies Meta<typeof TimeTrackerPanel>
94
+
95
+ export default meta
96
+ type Story = StoryObj<typeof meta>
97
+
98
+ interface HostInit {
99
+ status?: TimeTrackerStatus
100
+ runningSince?: number | null
101
+ accumulatedMs?: number
102
+ selectedTicketIds?: string[]
103
+ notes?: string
104
+ lastEntries?: TimeTrackerEntry[]
105
+ /** When true, ticket options are filtered asynchronously (server-search simulation). */
106
+ serverSearch?: boolean
107
+ }
108
+
109
+ function useTimeTrackerHost(initial: HostInit = {}): TimeTrackerData {
110
+ const [status, setStatus] = React.useState<TimeTrackerStatus>(initial.status ?? 'ready')
111
+ const [runningSince, setRunningSince] = React.useState<number | null>(initial.runningSince ?? null)
112
+ const [accumulatedMs, setAccumulatedMs] = React.useState(initial.accumulatedMs ?? 0)
113
+ const [selectedTicketIds, setSelectedTicketIds] = React.useState<string[]>(initial.selectedTicketIds ?? [])
114
+ const [notes, setNotes] = React.useState(initial.notes ?? '')
115
+ const [lastEntries, setLastEntries] = React.useState<TimeTrackerEntry[]>(initial.lastEntries ?? SEED_ENTRIES)
116
+
117
+ const [ticketOptions, setTicketOptions] = React.useState(ALL_TICKETS)
118
+ const [ticketsLoading, setTicketsLoading] = React.useState(false)
119
+ const searchRef = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
120
+
121
+ const currentElapsedMs = () =>
122
+ accumulatedMs + (status === 'tracking' && runningSince ? Date.now() - runningSince : 0)
123
+
124
+ const onTicketSearch = initial.serverSearch
125
+ ? (query: string) => {
126
+ setTicketsLoading(true)
127
+ clearTimeout(searchRef.current)
128
+ searchRef.current = setTimeout(() => {
129
+ const q = query.toLowerCase()
130
+ setTicketOptions(ALL_TICKETS.filter((t) => t.label.toLowerCase().includes(q)))
131
+ setTicketsLoading(false)
132
+ }, 350)
133
+ }
134
+ : undefined
135
+
136
+ return {
137
+ status,
138
+ runningSince,
139
+ accumulatedMs,
140
+ ticketOptions,
141
+ selectedTicketIds,
142
+ onSelectedTicketsChange: setSelectedTicketIds,
143
+ onTicketSearch,
144
+ ticketsLoading: initial.serverSearch ? ticketsLoading : undefined,
145
+ notes,
146
+ onNotesChange: setNotes,
147
+ lastEntries,
148
+ onStart: () => {
149
+ setAccumulatedMs(0)
150
+ setRunningSince(Date.now())
151
+ setStatus('tracking')
152
+ },
153
+ onPause: () => {
154
+ setAccumulatedMs(currentElapsedMs())
155
+ setRunningSince(null)
156
+ setStatus('paused')
157
+ },
158
+ onResume: () => {
159
+ setRunningSince(Date.now())
160
+ setStatus('tracking')
161
+ },
162
+ onCancel: () => {
163
+ setStatus('ready')
164
+ setRunningSince(null)
165
+ setAccumulatedMs(0)
166
+ setSelectedTicketIds([])
167
+ setNotes('')
168
+ },
169
+ onSubmit: () => {
170
+ const ms = currentElapsedMs()
171
+ const ticketId = selectedTicketIds[0]
172
+ const entry: TimeTrackerEntry = {
173
+ id: `entry-${Date.now()}`,
174
+ durationLabel: formatDuration(ms),
175
+ dateLabel: formatDate(new Date()),
176
+ title: ticketId
177
+ ? ALL_TICKETS.find((t) => t.id === ticketId)?.label ?? ticketId
178
+ : 'Manual time entry',
179
+ description: notes || undefined,
180
+ }
181
+ setLastEntries((prev) => [entry, ...prev].slice(0, 3))
182
+ setStatus('ready')
183
+ setRunningSince(null)
184
+ setAccumulatedMs(0)
185
+ setSelectedTicketIds([])
186
+ setNotes('')
187
+ },
188
+ onManualEntry: () => alert('Host: open the manual-entry form modal'),
189
+ onOpenMyTime: () => alert('Host: navigate to "My Time"'),
190
+ onOpenMyTimeMenu: () => alert('Host: open the "My Time" split menu'),
191
+ onEntryClick: (entry) => alert(`Host: navigate to entry "${entry.title}"`),
192
+ }
193
+ }
194
+
195
+ /** Mock header strip so the `HeaderButton` resolves its `h-full` and the popover anchors correctly. */
196
+ function HeaderBar() {
197
+ return (
198
+ <div className="flex h-14 w-full items-center justify-end border-b border-ods-border bg-ods-card divide-x divide-ods-border">
199
+ <TimeTrackerHeaderButton />
200
+ </div>
201
+ )
202
+ }
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // Panel stories — the popup content in each status. Fully interactive: the
206
+ // play/pause/stop buttons drive the same transitions the host would.
207
+ // ---------------------------------------------------------------------------
208
+
209
+ /** Idle. Stop button is hidden until tracking starts; pre-seeded last entries. */
210
+ export const PanelReady: Story = {
211
+ render: function PanelReadyRender() {
212
+ const host = useTimeTrackerHost({ status: 'ready' })
213
+ return (
214
+ <div className="max-w-[460px]">
215
+ <TimeTrackerPanel {...host} onClose={() => {}} />
216
+ </div>
217
+ )
218
+ },
219
+ }
220
+
221
+ /** Tracking — the timer ticks live and a ticket is selected, so Stop (finish) is enabled. */
222
+ export const PanelTracking: Story = {
223
+ render: function PanelTrackingRender() {
224
+ const host = useTimeTrackerHost({
225
+ status: 'tracking',
226
+ runningSince: Date.now() - 5_000,
227
+ selectedTicketIds: ['TICK-102'],
228
+ })
229
+ return (
230
+ <div className="max-w-[460px]">
231
+ <TimeTrackerPanel {...host} onClose={() => {}} />
232
+ </div>
233
+ )
234
+ },
235
+ }
236
+
237
+ /** Paused at 00:08:17 with notes filled — Stop (finish) is enabled via the notes rule. */
238
+ export const PanelPaused: Story = {
239
+ render: function PanelPausedRender() {
240
+ const host = useTimeTrackerHost({
241
+ status: 'paused',
242
+ accumulatedMs: 497_000,
243
+ notes: 'Walked user through Storage Sense setup, scheduled monthly cleanup.',
244
+ })
245
+ return (
246
+ <div className="max-w-[460px]">
247
+ <TimeTrackerPanel {...host} onClose={() => {}} />
248
+ </div>
249
+ )
250
+ },
251
+ }
252
+
253
+ /**
254
+ * Tracking with no ticket and no notes — pressing Finish (the yellow check) surfaces inline
255
+ * validation errors on both fields instead of submitting. Add a ticket or a note and Finish succeeds.
256
+ */
257
+ export const PanelFinishGate: Story = {
258
+ render: function PanelFinishGateRender() {
259
+ const host = useTimeTrackerHost({ status: 'tracking', runningSince: Date.now() - 42_000 })
260
+ return (
261
+ <div className="max-w-[460px]">
262
+ <TimeTrackerPanel {...host} onClose={() => {}} />
263
+ </div>
264
+ )
265
+ },
266
+ }
267
+
268
+ /** Empty last-entries state. */
269
+ export const PanelNoEntries: Story = {
270
+ render: function PanelNoEntriesRender() {
271
+ const host = useTimeTrackerHost({ status: 'ready', lastEntries: [] })
272
+ return (
273
+ <div className="max-w-[460px]">
274
+ <TimeTrackerPanel {...host} onClose={() => {}} />
275
+ </div>
276
+ )
277
+ },
278
+ }
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // Header button stories — the trigger as it appears in the app header.
282
+ // ---------------------------------------------------------------------------
283
+
284
+ /** Idle header button — clock icon only. Click it to open the popup. */
285
+ export const HeaderButtonIdle: Story = {
286
+ parameters: { layout: 'fullscreen' },
287
+ render: function HeaderButtonIdleRender() {
288
+ const host = useTimeTrackerHost({ status: 'ready' })
289
+ return (
290
+ <TimeTrackerProvider {...host}>
291
+ <HeaderBar />
292
+ <p className="p-4 text-h6 text-ods-text-secondary">Click the clock icon to open the time tracker.</p>
293
+ </TimeTrackerProvider>
294
+ )
295
+ },
296
+ }
297
+
298
+ /** Active header button — shows the live elapsed time next to the clock icon. */
299
+ export const HeaderButtonTracking: Story = {
300
+ parameters: { layout: 'fullscreen' },
301
+ render: function HeaderButtonTrackingRender() {
302
+ const host = useTimeTrackerHost({ status: 'tracking', runningSince: Date.now() - 65_000 })
303
+ return (
304
+ <TimeTrackerProvider {...host}>
305
+ <HeaderBar />
306
+ <p className="p-4 text-h6 text-ods-text-secondary">
307
+ While tracking, the header shows the running timer. Click it to open the popup.
308
+ </p>
309
+ </TimeTrackerProvider>
310
+ )
311
+ },
312
+ }
313
+
314
+ /** Paused header button — clock icon and timer are greyed out (frozen elapsed time). */
315
+ export const HeaderButtonPaused: Story = {
316
+ parameters: { layout: 'fullscreen' },
317
+ render: function HeaderButtonPausedRender() {
318
+ const host = useTimeTrackerHost({ status: 'paused', accumulatedMs: 65_000 })
319
+ return (
320
+ <TimeTrackerProvider {...host}>
321
+ <HeaderBar />
322
+ <p className="p-4 text-h6 text-ods-text-secondary">
323
+ While paused, the clock and timer are muted to signal the session is on hold.
324
+ </p>
325
+ </TimeTrackerProvider>
326
+ )
327
+ },
328
+ }
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // Full interactive playground — provider + header button + simulated server
332
+ // ticket search + real start/pause/resume/finish that persists into Last Entries.
333
+ // ---------------------------------------------------------------------------
334
+
335
+ export const Playground: Story = {
336
+ parameters: { layout: 'fullscreen' },
337
+ render: function PlaygroundRender() {
338
+ const host = useTimeTrackerHost({ serverSearch: true })
339
+ return (
340
+ <TimeTrackerProvider {...host}>
341
+ <HeaderBar />
342
+ <div className="max-w-xl p-4">
343
+ <p className="text-h3 text-ods-text-primary">Time tracker playground</p>
344
+ <ul className="mt-2 list-disc pl-5 text-h6 text-ods-text-secondary">
345
+ <li>Open the popup from the clock button in the header above.</li>
346
+ <li>Press play to start; the header and panel timers tick from the start time.</li>
347
+ <li>Typing in “Assign Ticket” runs a simulated async search (loading spinner).</li>
348
+ <li>Stop is disabled until you pick a ticket or type a note.</li>
349
+ <li>Finishing pushes a new row into Last Entries (capped at 3) and resets to idle.</li>
350
+ </ul>
351
+ </div>
352
+ </TimeTrackerProvider>
353
+ )
354
+ },
355
+ }
@@ -11,7 +11,19 @@ export { getPlatformProductionUrl, getAllPlatformBaseDomains } from '../platform
11
11
  export { formatDate, formatNumber, formatPrice, formatBytes } from './format'
12
12
  // SVG path constants — re-exported here (server-safe) because icons-v2 has "use client"
13
13
  export { PLAY_ICON_PATH } from '../components/icons-v2-generated/media-playback/play-icon'
14
- export { getPlatformAccentColor, getCurrentPlatform, type ColorCategory, HEX_PATTERN } from './ods-color-utils'
14
+ export {
15
+ getPlatformAccentColor,
16
+ getCurrentPlatform,
17
+ type ColorCategory,
18
+ HEX_PATTERN,
19
+ // Custom-color helpers so consumers (e.g. the chat client's accent theming)
20
+ // reuse the design-system math instead of re-implementing darken/hex utils.
21
+ getReadableTextColor,
22
+ hexToRgb,
23
+ rgbToHex,
24
+ deriveHoverColor,
25
+ deriveActiveColor,
26
+ } from './ods-color-utils'
15
27
  export { delay, generateRandomString, truncateString, deepClone, getSlackCommunityJoinUrl, serializeJsonLd } from './common'
16
28
  export { getBaseUrl } from '../utils/cn'
17
29