@flamingo-stack/openframe-frontend-core 0.0.276 → 0.0.277-snapshot.20260617155519
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-62U6DFIT.js → chunk-5NL62LIF.js} +4 -4
- package/dist/{chunk-NPKMH446.js → chunk-737ZLQ2T.js} +3 -3
- package/dist/{chunk-IAB7QACX.cjs → chunk-7LTWNUHB.cjs} +1473 -1117
- 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-6IJWWLC3.cjs → chunk-BBDJCXHW.cjs} +10 -10
- package/dist/{chunk-6IJWWLC3.cjs.map → chunk-BBDJCXHW.cjs.map} +1 -1
- package/dist/{chunk-DPMW5E6Q.cjs → chunk-DO55GWNT.cjs} +19 -19
- package/dist/{chunk-DPMW5E6Q.cjs.map → chunk-DO55GWNT.cjs.map} +1 -1
- package/dist/{chunk-VAIPAZPJ.js → chunk-GO45CXT5.js} +2247 -1891
- 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-MYJKC56A.cjs → chunk-L4BJU5CE.cjs} +38 -38
- package/dist/{chunk-MYJKC56A.cjs.map → chunk-L4BJU5CE.cjs.map} +1 -1
- package/dist/{chunk-NITJSVUM.js → chunk-MDHNLZME.js} +4 -4
- package/dist/{chunk-QNWFTSAG.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-SITWWJWS.cjs → chunk-WYQ5VFXA.cjs} +29 -29
- package/dist/{chunk-SITWWJWS.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/chat-container.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +4 -4
- package/dist/components/chat/index.js +3 -3
- package/dist/components/chat/types/component.types.d.ts +11 -0
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- 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/chat/chat-container.tsx +40 -25
- package/src/components/chat/types/component.types.ts +11 -0
- 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-IAB7QACX.cjs.map +0 -1
- package/dist/chunk-LH7R56RR.cjs.map +0 -1
- package/dist/chunk-N5UOJC3V.cjs.map +0 -1
- package/dist/chunk-VAIPAZPJ.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-62U6DFIT.js.map → chunk-5NL62LIF.js.map} +0 -0
- /package/dist/{chunk-NPKMH446.js.map → chunk-737ZLQ2T.js.map} +0 -0
- /package/dist/{chunk-NITJSVUM.js.map → chunk-MDHNLZME.js.map} +0 -0
- /package/dist/{chunk-QNWFTSAG.js.map → chunk-MVUN35FH.js.map} +0 -0
- /package/dist/{chunk-UCX75VEM.js.map → chunk-R6SHY6ZQ.js.map} +0 -0
|
@@ -4,6 +4,7 @@ import React from 'react'
|
|
|
4
4
|
import { useMdUp } from '../../hooks/ui/use-media-query'
|
|
5
5
|
import { cn } from '../../utils/cn'
|
|
6
6
|
import { useOptionalNotifications } from '../features/notifications/notifications-context'
|
|
7
|
+
import { TimeTrackerHeaderButton } from '../features/time-tracker'
|
|
7
8
|
import { LogOutIcon, OpenFrameLogo, OpenFrameText, UserIcon } from '../icons'
|
|
8
9
|
import { BellIcon } from '../icons-v2-generated/interface/bell-icon'
|
|
9
10
|
import { Menu01Icon, SearchIcon, XmarkIcon } from '../icons-v2-generated'
|
|
@@ -22,6 +23,8 @@ export interface AppHeaderProps {
|
|
|
22
23
|
onOrgChange?: (id: string) => void
|
|
23
24
|
showNotifications?: boolean
|
|
24
25
|
unreadCount?: number
|
|
26
|
+
/** Render the time-tracker button + popup. Requires wrapping the app in `<TimeTrackerProvider>`. */
|
|
27
|
+
showTimeTracker?: boolean
|
|
25
28
|
/** Render the "Mingo AI" launcher button (drawer-style trigger for an
|
|
26
29
|
* in-layout `AppLayoutDrawer` hosting the chat panel). Defaults to off. */
|
|
27
30
|
showMingoAI?: boolean
|
|
@@ -57,6 +60,7 @@ export const AppHeader = React.memo(function AppHeader({
|
|
|
57
60
|
onOrgChange,
|
|
58
61
|
showNotifications,
|
|
59
62
|
unreadCount = 0,
|
|
63
|
+
showTimeTracker = false,
|
|
60
64
|
showMingoAI = false,
|
|
61
65
|
onMingoAI,
|
|
62
66
|
isMingoAIActive = false,
|
|
@@ -129,6 +133,11 @@ export const AppHeader = React.memo(function AppHeader({
|
|
|
129
133
|
/>
|
|
130
134
|
)}
|
|
131
135
|
|
|
136
|
+
{/* Time tracker button */}
|
|
137
|
+
{showTimeTracker && (
|
|
138
|
+
<TimeTrackerHeaderButton disabled={disabled} className={dimmedClass} />
|
|
139
|
+
)}
|
|
140
|
+
|
|
132
141
|
{/* Notifications button */}
|
|
133
142
|
{showNotifications && (
|
|
134
143
|
<NotificationsHeaderButton
|
|
@@ -635,18 +635,13 @@ function AutocompleteInner<T = string>(
|
|
|
635
635
|
<div className="relative" ref={containerRef}>
|
|
636
636
|
{popover}
|
|
637
637
|
|
|
638
|
-
{/* Hidden tags popup — outside overflow-hidden
|
|
638
|
+
{/* Hidden tags popup — outside overflow-hidden; right-anchored to the field */}
|
|
639
639
|
{multiple && showHiddenTags && hiddenTagsCount > 0 && (
|
|
640
640
|
<HiddenTagsPopup
|
|
641
641
|
ref={hiddenTagsPopupRef}
|
|
642
642
|
items={hiddenTags}
|
|
643
643
|
disabled={disabled}
|
|
644
|
-
|
|
645
|
-
left: autoLimitTags.badgeRef.current
|
|
646
|
-
? autoLimitTags.badgeRef.current.getBoundingClientRect().left -
|
|
647
|
-
(containerRef.current?.getBoundingClientRect().left ?? 0)
|
|
648
|
-
: 0,
|
|
649
|
-
}}
|
|
644
|
+
className="left-auto right-0 max-w-full"
|
|
650
645
|
onRemove={(value) => {
|
|
651
646
|
const newValue = valueArray.filter(v => v !== value)
|
|
652
647
|
;(props as AutocompleteMultipleProps<T>).onChange(newValue)
|
|
@@ -26,6 +26,8 @@ export const HiddenTagsPopup = forwardRef(function HiddenTagsPopup(
|
|
|
26
26
|
ref={ref}
|
|
27
27
|
style={style}
|
|
28
28
|
className={cn(
|
|
29
|
+
// Base positioning is neutral (left-anchored); consumers override via
|
|
30
|
+
// `style.left` (search-input, tag-search-input) or `className` (autocomplete).
|
|
29
31
|
"absolute top-full left-0 mt-1 z-50 min-w-[200px]",
|
|
30
32
|
"bg-ods-card border border-ods-border rounded-[6px] shadow-lg",
|
|
31
33
|
"animate-in fade-in-0 zoom-in-95 duration-150",
|
|
@@ -40,7 +42,7 @@ export const HiddenTagsPopup = forwardRef(function HiddenTagsPopup(
|
|
|
40
42
|
"border-b border-ods-border last:border-b-0",
|
|
41
43
|
)}
|
|
42
44
|
>
|
|
43
|
-
<span className="text-h5 truncate uppercase text-ods-text-primary" title={typeof item.label === 'string' ? item.label : undefined}>
|
|
45
|
+
<span className="min-w-0 flex-1 text-h5 truncate uppercase text-ods-text-primary" title={typeof item.label === 'string' ? item.label : undefined}>
|
|
44
46
|
{item.label}
|
|
45
47
|
</span>
|
|
46
48
|
{!disabled && onRemove && (
|
|
@@ -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
|
|