@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.
- package/dist/{chunk-LDEXTWMK.js → chunk-5IJ46KAV.js} +2 -1
- package/dist/{chunk-MHQPAA3W.js → chunk-5NL62LIF.js} +4 -4
- package/dist/{chunk-KBKW4LVO.js → chunk-737ZLQ2T.js} +3 -3
- package/dist/{chunk-LALAS7OH.cjs → chunk-7LTWNUHB.cjs} +1465 -1115
- package/dist/chunk-7LTWNUHB.cjs.map +1 -0
- package/dist/{chunk-2GLHJ7ZY.cjs → chunk-7XUBAK5K.cjs} +12 -12
- package/dist/{chunk-2GLHJ7ZY.cjs.map → chunk-7XUBAK5K.cjs.map} +1 -1
- package/dist/{chunk-L5P4IFPK.cjs → chunk-BBDJCXHW.cjs} +10 -10
- package/dist/{chunk-L5P4IFPK.cjs.map → chunk-BBDJCXHW.cjs.map} +1 -1
- package/dist/{chunk-NCJENC5G.cjs → chunk-DO55GWNT.cjs} +19 -19
- package/dist/{chunk-NCJENC5G.cjs.map → chunk-DO55GWNT.cjs.map} +1 -1
- package/dist/{chunk-P6Y3S6QK.js → chunk-GO45CXT5.js} +2239 -1889
- package/dist/chunk-GO45CXT5.js.map +1 -0
- package/dist/{chunk-LH7R56RR.cjs → chunk-HR4CVAVH.cjs} +20 -18
- package/dist/chunk-HR4CVAVH.cjs.map +1 -0
- package/dist/{chunk-ZSPPV2SX.cjs → chunk-L4BJU5CE.cjs} +38 -38
- package/dist/{chunk-ZSPPV2SX.cjs.map → chunk-L4BJU5CE.cjs.map} +1 -1
- package/dist/{chunk-DMAPXENZ.js → chunk-MDHNLZME.js} +4 -4
- package/dist/{chunk-6A5GGUIA.js → chunk-MVUN35FH.js} +3 -3
- package/dist/{chunk-UCX75VEM.js → chunk-R6SHY6ZQ.js} +2 -2
- package/dist/{chunk-ZS4XNIZK.js → chunk-RGCVYGA2.js} +5 -3
- package/dist/chunk-RGCVYGA2.js.map +1 -0
- package/dist/{chunk-27NKVNWD.cjs → chunk-WYQ5VFXA.cjs} +29 -29
- package/dist/{chunk-27NKVNWD.cjs.map → chunk-WYQ5VFXA.cjs.map} +1 -1
- package/dist/{chunk-N5UOJC3V.cjs → chunk-XXI7BNB6.cjs} +3 -2
- package/dist/chunk-XXI7BNB6.cjs.map +1 -0
- package/dist/components/chat/index.cjs +4 -4
- package/dist/components/chat/index.js +3 -3
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/faq/index.js +4 -4
- package/dist/components/features/index.cjs +16 -4
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.d.ts +1 -0
- package/dist/components/features/index.d.ts.map +1 -1
- package/dist/components/features/index.js +16 -4
- package/dist/components/features/time-tracker/index.d.ts +10 -0
- package/dist/components/features/time-tracker/index.d.ts.map +1 -0
- package/dist/components/features/time-tracker/time-tracker-context.d.ts +17 -0
- package/dist/components/features/time-tracker/time-tracker-context.d.ts.map +1 -0
- package/dist/components/features/time-tracker/time-tracker-header-button.d.ts +13 -0
- package/dist/components/features/time-tracker/time-tracker-header-button.d.ts.map +1 -0
- package/dist/components/features/time-tracker/time-tracker-panel.d.ts +7 -0
- package/dist/components/features/time-tracker/time-tracker-panel.d.ts.map +1 -0
- package/dist/components/features/time-tracker/types.d.ts +54 -0
- package/dist/components/features/time-tracker/types.d.ts.map +1 -0
- package/dist/components/features/time-tracker/use-tracker-clock.d.ts +14 -0
- package/dist/components/features/time-tracker/use-tracker-clock.d.ts.map +1 -0
- package/dist/components/index.cjs +114 -102
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +19 -7
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/app-header.d.ts +2 -0
- package/dist/components/navigation/app-header.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/onboarding-guides/index.cjs +23 -23
- package/dist/components/onboarding-guides/index.js +3 -3
- package/dist/components/related-content/index.cjs +5 -5
- package/dist/components/related-content/index.js +4 -4
- package/dist/components/tickets/index.cjs +72 -72
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/autocomplete.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +20 -20
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/hidden-tags-popup.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +4 -4
- package/dist/components/ui/index.js +3 -3
- package/dist/index.cjs +24 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +23 -3
- package/dist/utils/index.cjs +37 -4
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +34 -5
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/features/index.ts +1 -0
- package/src/components/features/time-tracker/index.ts +18 -0
- package/src/components/features/time-tracker/time-tracker-context.tsx +45 -0
- package/src/components/features/time-tracker/time-tracker-header-button.tsx +87 -0
- package/src/components/features/time-tracker/time-tracker-panel.tsx +270 -0
- package/src/components/features/time-tracker/types.ts +61 -0
- package/src/components/features/time-tracker/use-tracker-clock.ts +39 -0
- package/src/components/navigation/app-header.tsx +9 -0
- package/src/components/ui/autocomplete.tsx +2 -7
- package/src/components/ui/hidden-tags-popup.tsx +3 -1
- package/src/stories/TimeTracker.stories.tsx +355 -0
- package/src/utils/index.ts +13 -1
- package/dist/chunk-LALAS7OH.cjs.map +0 -1
- package/dist/chunk-LH7R56RR.cjs.map +0 -1
- package/dist/chunk-N5UOJC3V.cjs.map +0 -1
- package/dist/chunk-P6Y3S6QK.js.map +0 -1
- package/dist/chunk-ZS4XNIZK.js.map +0 -1
- /package/dist/{chunk-LDEXTWMK.js.map → chunk-5IJ46KAV.js.map} +0 -0
- /package/dist/{chunk-MHQPAA3W.js.map → chunk-5NL62LIF.js.map} +0 -0
- /package/dist/{chunk-KBKW4LVO.js.map → chunk-737ZLQ2T.js.map} +0 -0
- /package/dist/{chunk-DMAPXENZ.js.map → chunk-MDHNLZME.js.map} +0 -0
- /package/dist/{chunk-6A5GGUIA.js.map → chunk-MVUN35FH.js.map} +0 -0
- /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
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
|