@flamingo-stack/openframe-frontend-core 0.0.316 → 0.0.317
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-L5JSGNT3.cjs → chunk-27IO7V3P.cjs} +9 -9
- package/dist/{chunk-L5JSGNT3.cjs.map → chunk-27IO7V3P.cjs.map} +1 -1
- package/dist/{chunk-ZYGVJXJ5.cjs → chunk-2SIG5WZQ.cjs} +19 -19
- package/dist/{chunk-ZYGVJXJ5.cjs.map → chunk-2SIG5WZQ.cjs.map} +1 -1
- package/dist/{chunk-63A53WQN.cjs → chunk-3CW4QIUC.cjs} +29 -29
- package/dist/{chunk-63A53WQN.cjs.map → chunk-3CW4QIUC.cjs.map} +1 -1
- package/dist/{chunk-GHVVOST5.js → chunk-4APKC3JB.js} +5 -5
- package/dist/{chunk-OLEW7FYZ.cjs → chunk-4FX56H4X.cjs} +39 -39
- package/dist/{chunk-OLEW7FYZ.cjs.map → chunk-4FX56H4X.cjs.map} +1 -1
- package/dist/{chunk-J7AV6H63.js → chunk-4MD2TPFY.js} +75 -75
- package/dist/{chunk-JEBL5PQK.js → chunk-5EV3GLZK.js} +4 -4
- package/dist/{chunk-6KERXOFE.js → chunk-5EZ3WDDL.js} +3 -3
- package/dist/{chunk-64DZ2J7Q.js → chunk-6W3MPA7N.js} +3 -3
- package/dist/{chunk-Q4AMYLKX.js → chunk-7KZJV4MP.js} +2 -2
- package/dist/{chunk-JAZM3A7E.js → chunk-A6NBGRGZ.js} +2 -2
- package/dist/{chunk-PIJ4JLJU.js → chunk-ALP347PU.js} +6 -6
- package/dist/{chunk-CUNMBP3A.cjs → chunk-B43HTMWN.cjs} +13 -13
- package/dist/{chunk-CUNMBP3A.cjs.map → chunk-B43HTMWN.cjs.map} +1 -1
- package/dist/{chunk-4MCMPYEM.cjs → chunk-BMCO4Z6Y.cjs} +12 -12
- package/dist/{chunk-4MCMPYEM.cjs.map → chunk-BMCO4Z6Y.cjs.map} +1 -1
- package/dist/{chunk-VCE3ZEN3.cjs → chunk-EBL2SQ6P.cjs} +5 -5
- package/dist/{chunk-VCE3ZEN3.cjs.map → chunk-EBL2SQ6P.cjs.map} +1 -1
- package/dist/{chunk-FCEVVNWY.cjs → chunk-EQTU5KRP.cjs} +73 -73
- package/dist/{chunk-FCEVVNWY.cjs.map → chunk-EQTU5KRP.cjs.map} +1 -1
- package/dist/{chunk-2Y4DLBFO.js → chunk-FVXOKJ4B.js} +5087 -4887
- package/dist/chunk-FVXOKJ4B.js.map +1 -0
- package/dist/{chunk-ZS2SBWBR.cjs → chunk-GXRWE7NB.cjs} +2 -2
- package/dist/{chunk-ZS2SBWBR.cjs.map → chunk-GXRWE7NB.cjs.map} +1 -1
- package/dist/{chunk-XAQJ4ZLY.cjs → chunk-IFG6RJ5S.cjs} +1098 -898
- package/dist/chunk-IFG6RJ5S.cjs.map +1 -0
- package/dist/{chunk-DHVL36CA.cjs → chunk-JCTNHTPT.cjs} +40 -40
- package/dist/{chunk-DHVL36CA.cjs.map → chunk-JCTNHTPT.cjs.map} +1 -1
- package/dist/{chunk-CSLMCBZV.js → chunk-JSX6QQ7Z.js} +6 -6
- package/dist/{chunk-UGDGUO26.cjs → chunk-K5HBVPJ5.cjs} +3 -3
- package/dist/{chunk-UGDGUO26.cjs.map → chunk-K5HBVPJ5.cjs.map} +1 -1
- package/dist/{chunk-M3NULYCR.js → chunk-LDBS7C7H.js} +5 -5
- package/dist/{chunk-Z3YORGG4.js → chunk-OG6COCMK.js} +2 -2
- package/dist/{chunk-FOVX3W3C.cjs → chunk-OY5ZZLAN.cjs} +70 -70
- package/dist/{chunk-FOVX3W3C.cjs.map → chunk-OY5ZZLAN.cjs.map} +1 -1
- package/dist/{chunk-4V3TCOFC.cjs → chunk-PU22RRNK.cjs} +31 -31
- package/dist/{chunk-4V3TCOFC.cjs.map → chunk-PU22RRNK.cjs.map} +1 -1
- package/dist/{chunk-YFGDZFUG.js → chunk-ULEUTOOA.js} +3 -3
- package/dist/{chunk-LAMDFGE3.cjs → chunk-XHBX7LG2.cjs} +15 -15
- package/dist/{chunk-LAMDFGE3.cjs.map → chunk-XHBX7LG2.cjs.map} +1 -1
- package/dist/{chunk-4NVA6W3J.js → chunk-XHPONEBL.js} +2 -2
- package/dist/{chunk-OKGZK6TT.js → chunk-ZARQHUES.js} +3 -3
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/index.cjs +6 -4
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +5 -3
- package/dist/components/chat/msp-organization-card.d.ts +33 -0
- package/dist/components/chat/msp-organization-card.d.ts.map +1 -0
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/features/time-tracker/time-tracker-header-button.d.ts.map +1 -1
- package/dist/components/features/time-tracker/time-tracker-panel.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/icons-v2-generated/index.cjs +2 -2
- package/dist/components/icons-v2-generated/index.js +1 -1
- package/dist/components/index.cjs +129 -125
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +16 -12
- package/dist/components/index.js.map +1 -1
- 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 +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/date-picker.d.ts.map +1 -1
- package/dist/components/ui/duration-input.d.ts +14 -0
- package/dist/components/ui/duration-input.d.ts.map +1 -0
- package/dist/components/ui/index.cjs +8 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +7 -3
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/hooks/use-toast.d.ts +1 -1
- package/dist/index.cjs +8 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -3
- package/package.json +1 -1
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/msp-organization-card.tsx +81 -0
- package/src/components/features/time-tracker/time-tracker-header-button.tsx +2 -1
- package/src/components/features/time-tracker/time-tracker-panel.tsx +10 -4
- package/src/components/navigation/app-header.tsx +4 -1
- package/src/components/ui/date-picker.tsx +56 -16
- package/src/components/ui/duration-input.tsx +142 -0
- package/src/components/ui/index.ts +1 -0
- package/src/stories/MspOrganizationCard.stories.tsx +80 -0
- package/dist/chunk-2Y4DLBFO.js.map +0 -1
- package/dist/chunk-XAQJ4ZLY.cjs.map +0 -1
- /package/dist/{chunk-GHVVOST5.js.map → chunk-4APKC3JB.js.map} +0 -0
- /package/dist/{chunk-J7AV6H63.js.map → chunk-4MD2TPFY.js.map} +0 -0
- /package/dist/{chunk-JEBL5PQK.js.map → chunk-5EV3GLZK.js.map} +0 -0
- /package/dist/{chunk-6KERXOFE.js.map → chunk-5EZ3WDDL.js.map} +0 -0
- /package/dist/{chunk-64DZ2J7Q.js.map → chunk-6W3MPA7N.js.map} +0 -0
- /package/dist/{chunk-Q4AMYLKX.js.map → chunk-7KZJV4MP.js.map} +0 -0
- /package/dist/{chunk-JAZM3A7E.js.map → chunk-A6NBGRGZ.js.map} +0 -0
- /package/dist/{chunk-PIJ4JLJU.js.map → chunk-ALP347PU.js.map} +0 -0
- /package/dist/{chunk-CSLMCBZV.js.map → chunk-JSX6QQ7Z.js.map} +0 -0
- /package/dist/{chunk-M3NULYCR.js.map → chunk-LDBS7C7H.js.map} +0 -0
- /package/dist/{chunk-Z3YORGG4.js.map → chunk-OG6COCMK.js.map} +0 -0
- /package/dist/{chunk-YFGDZFUG.js.map → chunk-ULEUTOOA.js.map} +0 -0
- /package/dist/{chunk-4NVA6W3J.js.map → chunk-XHPONEBL.js.map} +0 -0
- /package/dist/{chunk-OKGZK6TT.js.map → chunk-ZARQHUES.js.map} +0 -0
|
@@ -91,6 +91,10 @@ export function TimeTrackerPanel({
|
|
|
91
91
|
onOpenMyTime()
|
|
92
92
|
}
|
|
93
93
|
: undefined
|
|
94
|
+
const handleCancel = () => {
|
|
95
|
+
onClose()
|
|
96
|
+
onCancel()
|
|
97
|
+
}
|
|
94
98
|
|
|
95
99
|
const ticketAutocompleteOptions: AutocompleteOption[] = ticketOptions.map((t) => ({
|
|
96
100
|
label: t.label,
|
|
@@ -123,7 +127,7 @@ export function TimeTrackerPanel({
|
|
|
123
127
|
{isActive && (
|
|
124
128
|
<Button
|
|
125
129
|
variant="transparent"
|
|
126
|
-
onClick={
|
|
130
|
+
onClick={handleCancel}
|
|
127
131
|
className="h-auto p-0 text-h6 font-medium text-ods-text-secondary underline hover:bg-transparent hover:text-ods-text-primary md:h-auto"
|
|
128
132
|
>
|
|
129
133
|
Cancel Entry
|
|
@@ -135,7 +139,7 @@ export function TimeTrackerPanel({
|
|
|
135
139
|
<div className="flex flex-1 items-center border-r border-ods-border bg-ods-bg px-[var(--spacing-system-m)] py-[var(--spacing-system-s)]">
|
|
136
140
|
<span
|
|
137
141
|
className={cn(
|
|
138
|
-
'text-h3 font-bold tabular-nums',
|
|
142
|
+
'text-h3 !font-mono font-bold tabular-nums',
|
|
139
143
|
isActive ? 'text-ods-text-primary' : 'text-ods-text-secondary',
|
|
140
144
|
)}
|
|
141
145
|
>
|
|
@@ -209,10 +213,10 @@ export function TimeTrackerPanel({
|
|
|
209
213
|
</div>
|
|
210
214
|
</div>
|
|
211
215
|
|
|
212
|
-
<div className="
|
|
216
|
+
<div className="grid grid-cols-2 gap-[var(--spacing-system-m)]">
|
|
213
217
|
<Button
|
|
214
218
|
variant="outline"
|
|
215
|
-
className="min-w-0
|
|
219
|
+
className="min-w-0"
|
|
216
220
|
onClick={handleManualEntry}
|
|
217
221
|
disabled={!onManualEntry}
|
|
218
222
|
leftIcon={<PlusCircleIcon className="text-ods-text-secondary" />}
|
|
@@ -221,6 +225,8 @@ export function TimeTrackerPanel({
|
|
|
221
225
|
</Button>
|
|
222
226
|
<SplitButton
|
|
223
227
|
variant="outline"
|
|
228
|
+
fullWidth
|
|
229
|
+
className="min-w-0"
|
|
224
230
|
onClick={handleOpenMyTime}
|
|
225
231
|
disabled={!onOpenMyTime && !onOpenMyTimeMenu}
|
|
226
232
|
mainDisabled={!onOpenMyTime}
|
|
@@ -160,7 +160,10 @@ export const AppHeader = React.memo(function AppHeader({
|
|
|
160
160
|
/>}
|
|
161
161
|
aria-label="User"
|
|
162
162
|
disabled={disabled}
|
|
163
|
-
className={
|
|
163
|
+
className={cn(
|
|
164
|
+
'outline-none ring-0 focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0',
|
|
165
|
+
dimmedClass,
|
|
166
|
+
)}
|
|
164
167
|
/>
|
|
165
168
|
</DropdownMenuTrigger>
|
|
166
169
|
<DropdownMenuContent
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type DateRange,
|
|
9
9
|
type DayPickerProps,
|
|
10
10
|
} from "react-day-picker";
|
|
11
|
+
import { useMdUp } from "../../hooks";
|
|
11
12
|
import { cn } from "../../utils/cn";
|
|
12
13
|
import { Button } from "./button";
|
|
13
14
|
import { FieldWrapper } from "./field-wrapper";
|
|
@@ -133,14 +134,46 @@ function DatePickerCalendar({
|
|
|
133
134
|
}: DatePickerCalendarProps) {
|
|
134
135
|
const today = new Date();
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
const
|
|
137
|
+
const isMdUp = useMdUp() ?? true;
|
|
138
|
+
const monthsToShow = isMdUp ? numberOfMonths : 1;
|
|
139
|
+
|
|
140
|
+
const [draftRange, setDraftRange] = React.useState<DateRange | undefined>(
|
|
141
|
+
mode === "range" ? (selected as DateRange | undefined) : undefined
|
|
142
|
+
);
|
|
143
|
+
const [hoveredDate, setHoveredDate] = React.useState<Date | undefined>(undefined);
|
|
144
|
+
|
|
145
|
+
const rangeSelected = draftRange;
|
|
138
146
|
const hasCompleteRange =
|
|
139
147
|
mode === "range" &&
|
|
140
|
-
rangeSelected?.from &&
|
|
141
|
-
rangeSelected?.to &&
|
|
148
|
+
!!rangeSelected?.from &&
|
|
149
|
+
!!rangeSelected?.to &&
|
|
142
150
|
rangeSelected.from.getTime() !== rangeSelected.to.getTime();
|
|
143
151
|
|
|
152
|
+
const isPreviewDate = (date: Date): boolean => {
|
|
153
|
+
if (!draftRange?.from || draftRange.to || !hoveredDate) return false;
|
|
154
|
+
const start = Math.min(draftRange.from.getTime(), hoveredDate.getTime());
|
|
155
|
+
const end = Math.max(draftRange.from.getTime(), hoveredDate.getTime());
|
|
156
|
+
return date.getTime() >= start && date.getTime() <= end;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const handleRangeSelect = (triggerDate: Date | undefined): void => {
|
|
160
|
+
if (!triggerDate) return;
|
|
161
|
+
|
|
162
|
+
if (!draftRange?.from || draftRange.to) {
|
|
163
|
+
setDraftRange({ from: triggerDate, to: undefined });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Second click closes the range, ordering the two ends.
|
|
167
|
+
const start = draftRange.from;
|
|
168
|
+
const completed: DateRange =
|
|
169
|
+
triggerDate.getTime() < start.getTime()
|
|
170
|
+
? { from: triggerDate, to: start }
|
|
171
|
+
: { from: start, to: triggerDate };
|
|
172
|
+
setDraftRange(completed);
|
|
173
|
+
setHoveredDate(undefined);
|
|
174
|
+
onSelect(completed);
|
|
175
|
+
};
|
|
176
|
+
|
|
144
177
|
const classNames: DayPickerProps["classNames"] = {
|
|
145
178
|
root: "p-4 date-picker-calendar",
|
|
146
179
|
months: "flex gap-8",
|
|
@@ -294,7 +327,10 @@ function DatePickerCalendar({
|
|
|
294
327
|
|
|
295
328
|
// Range mode
|
|
296
329
|
return (
|
|
297
|
-
<div
|
|
330
|
+
<div
|
|
331
|
+
className="bg-ods-card border border-ods-border rounded-md overflow-hidden"
|
|
332
|
+
onMouseLeave={() => setHoveredDate(undefined)}
|
|
333
|
+
>
|
|
298
334
|
<style>{rangeStyles}</style>
|
|
299
335
|
<div className="flex">
|
|
300
336
|
{/* First month */}
|
|
@@ -308,19 +344,22 @@ function DatePickerCalendar({
|
|
|
308
344
|
<span className="text-h4 text-ods-text-primary">
|
|
309
345
|
{formatMonthYear(month)}
|
|
310
346
|
</span>
|
|
311
|
-
{
|
|
347
|
+
{monthsToShow === 1 && (
|
|
312
348
|
<CalendarNavButton
|
|
313
349
|
direction="right"
|
|
314
350
|
onClick={handleNextMonth}
|
|
315
351
|
aria-label="Next month"
|
|
316
352
|
/>
|
|
317
353
|
)}
|
|
318
|
-
{
|
|
354
|
+
{monthsToShow === 2 && <div className="size-10 md:size-12" />}
|
|
319
355
|
</div>
|
|
320
356
|
<DayPicker
|
|
321
357
|
mode="range"
|
|
322
|
-
selected={
|
|
323
|
-
onSelect={(
|
|
358
|
+
selected={draftRange}
|
|
359
|
+
onSelect={(_range, triggerDate) => handleRangeSelect(triggerDate)}
|
|
360
|
+
onDayMouseEnter={(day) => setHoveredDate(day)}
|
|
361
|
+
modifiers={{ preview: isPreviewDate }}
|
|
362
|
+
modifiersClassNames={{ preview: "bg-ods-bg-surface" }}
|
|
324
363
|
month={month}
|
|
325
364
|
onMonthChange={setMonth}
|
|
326
365
|
classNames={classNames}
|
|
@@ -333,8 +372,8 @@ function DatePickerCalendar({
|
|
|
333
372
|
/>
|
|
334
373
|
</div>
|
|
335
374
|
|
|
336
|
-
{/* Second month (if
|
|
337
|
-
{
|
|
375
|
+
{/* Second month (if monthsToShow === 2) */}
|
|
376
|
+
{monthsToShow === 2 && (
|
|
338
377
|
<div className="flex-1 border-l border-ods-border">
|
|
339
378
|
<div className="flex items-center justify-between px-4 pt-4">
|
|
340
379
|
<div className="size-10 md:size-12" />
|
|
@@ -349,8 +388,11 @@ function DatePickerCalendar({
|
|
|
349
388
|
</div>
|
|
350
389
|
<DayPicker
|
|
351
390
|
mode="range"
|
|
352
|
-
selected={
|
|
353
|
-
onSelect={(
|
|
391
|
+
selected={draftRange}
|
|
392
|
+
onSelect={(_range, triggerDate) => handleRangeSelect(triggerDate)}
|
|
393
|
+
onDayMouseEnter={(day) => setHoveredDate(day)}
|
|
394
|
+
modifiers={{ preview: isPreviewDate }}
|
|
395
|
+
modifiersClassNames={{ preview: "bg-ods-bg-surface" }}
|
|
354
396
|
month={getSecondMonth(month)}
|
|
355
397
|
classNames={classNames}
|
|
356
398
|
showOutsideDays
|
|
@@ -438,9 +480,7 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
438
480
|
} else {
|
|
439
481
|
const range = value as DateRange | undefined;
|
|
440
482
|
props.onChange?.(range);
|
|
441
|
-
|
|
442
|
-
// Don't close when user just selected the start date
|
|
443
|
-
if (range?.from && range?.to && range.from.getTime() !== range.to.getTime()) {
|
|
483
|
+
if (range?.from && range?.to) {
|
|
444
484
|
setOpen(false);
|
|
445
485
|
}
|
|
446
486
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Fragment, type KeyboardEvent, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { cn } from "../../utils/cn";
|
|
5
|
+
|
|
6
|
+
export interface DurationInputProps {
|
|
7
|
+
/** Duration as `HH:MM:SS`. */
|
|
8
|
+
value: string;
|
|
9
|
+
onChange: (value: string) => void;
|
|
10
|
+
invalid?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type Segments = [string, string, string];
|
|
15
|
+
|
|
16
|
+
const SEGMENT_META = [
|
|
17
|
+
{ label: "Hours", placeholder: "HH", max: 99 },
|
|
18
|
+
{ label: "Minutes", placeholder: "MM", max: 59 },
|
|
19
|
+
{ label: "Seconds", placeholder: "SS", max: 59 },
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
const pad2 = (s: string): string => s.padStart(2, "0").slice(-2);
|
|
23
|
+
const compose = (s: Segments): string => `${pad2(s[0])}:${pad2(s[1])}:${pad2(s[2])}`;
|
|
24
|
+
const split = (value: string): Segments => {
|
|
25
|
+
const parts = value.split(":");
|
|
26
|
+
return [parts[0] || "00", parts[1] || "00", parts[2] || "00"];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Segmented `HH:MM:SS` entry: each unit is its own field, typing fills the focused
|
|
31
|
+
* segment and auto-advances, arrows move/increment. Segments clamp to their max
|
|
32
|
+
* (MM/SS ≤ 59), so an invalid duration can't be produced. Value in/out is `HH:MM:SS`.
|
|
33
|
+
*/
|
|
34
|
+
export function DurationInput({ value, onChange, invalid, className }: DurationInputProps) {
|
|
35
|
+
const [segments, setSegments] = useState<Segments>(() => split(value));
|
|
36
|
+
const refs = [useRef<HTMLInputElement>(null), useRef<HTMLInputElement>(null), useRef<HTMLInputElement>(null)];
|
|
37
|
+
|
|
38
|
+
// Adopt an externally-set value (edit pre-fill / form reset) without clobbering in-progress typing.
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
setSegments((prev) => (compose(prev) === value ? prev : split(value)));
|
|
41
|
+
}, [value]);
|
|
42
|
+
|
|
43
|
+
const focusSegment = (i: number) => {
|
|
44
|
+
const el = refs[i]?.current;
|
|
45
|
+
if (!el) return;
|
|
46
|
+
el.focus();
|
|
47
|
+
el.select();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const commit = (next: Segments, advanceFrom?: number) => {
|
|
51
|
+
setSegments(next);
|
|
52
|
+
onChange(compose(next));
|
|
53
|
+
if (advanceFrom !== undefined && advanceFrom < 2) focusSegment(advanceFrom + 1);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleChange = (i: number) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
57
|
+
const digits = event.target.value.replace(/\D/g, "").slice(-2);
|
|
58
|
+
const { max } = SEGMENT_META[i];
|
|
59
|
+
const next = [...segments] as Segments;
|
|
60
|
+
|
|
61
|
+
if (digits.length === 2) {
|
|
62
|
+
next[i] = String(Math.min(Number(digits), max));
|
|
63
|
+
commit(next, i);
|
|
64
|
+
} else if (digits.length === 1 && max === 59 && Number(digits) > 5) {
|
|
65
|
+
// 6–9 can't start a valid two-digit minute/second, so commit and advance now.
|
|
66
|
+
next[i] = digits;
|
|
67
|
+
commit(next, i);
|
|
68
|
+
} else {
|
|
69
|
+
next[i] = digits;
|
|
70
|
+
commit(next);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleKeyDown = (i: number) => (event: KeyboardEvent<HTMLInputElement>) => {
|
|
75
|
+
const el = event.currentTarget;
|
|
76
|
+
const { max } = SEGMENT_META[i];
|
|
77
|
+
|
|
78
|
+
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
const delta = event.key === "ArrowUp" ? 1 : -1;
|
|
81
|
+
const nextValue = (Number(segments[i] || "0") + delta + (max + 1)) % (max + 1);
|
|
82
|
+
const next = [...segments] as Segments;
|
|
83
|
+
next[i] = String(nextValue);
|
|
84
|
+
commit(next);
|
|
85
|
+
requestAnimationFrame(() => el.select());
|
|
86
|
+
} else if (event.key === "ArrowLeft" && el.selectionStart === 0 && i > 0) {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
focusSegment(i - 1);
|
|
89
|
+
} else if (event.key === "ArrowRight" && el.selectionEnd === el.value.length && i < 2) {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
focusSegment(i + 1);
|
|
92
|
+
} else if (event.key === "Backspace" && el.value === "" && i > 0) {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
focusSegment(i - 1);
|
|
95
|
+
} else if (event.key === ":" && i < 2) {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
focusSegment(i + 1);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
onMouseDown={(event) => {
|
|
104
|
+
if (event.target === event.currentTarget) {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
focusSegment(0);
|
|
107
|
+
}
|
|
108
|
+
}}
|
|
109
|
+
className={cn(
|
|
110
|
+
"flex h-11 w-full items-center gap-[var(--spacing-system-xxs)] rounded-md border border-ods-border bg-ods-card px-[var(--spacing-system-sf)] transition-colors duration-200 md:h-12",
|
|
111
|
+
"cursor-text focus-within:border-ods-accent",
|
|
112
|
+
invalid && "border-ods-error focus-within:border-ods-error",
|
|
113
|
+
className,
|
|
114
|
+
)}
|
|
115
|
+
>
|
|
116
|
+
{SEGMENT_META.map((meta, i) => (
|
|
117
|
+
<Fragment key={meta.label}>
|
|
118
|
+
{i > 0 && <span className="text-ods-text-secondary">:</span>}
|
|
119
|
+
<input
|
|
120
|
+
ref={refs[i]}
|
|
121
|
+
inputMode="numeric"
|
|
122
|
+
size={2}
|
|
123
|
+
value={segments[i]}
|
|
124
|
+
onChange={handleChange(i)}
|
|
125
|
+
onKeyDown={handleKeyDown(i)}
|
|
126
|
+
onFocus={(event) => event.currentTarget.select()}
|
|
127
|
+
onBlur={() =>
|
|
128
|
+
setSegments((prev) => {
|
|
129
|
+
const padded = [...prev] as Segments;
|
|
130
|
+
padded[i] = pad2(prev[i]);
|
|
131
|
+
return padded;
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
placeholder={meta.placeholder}
|
|
135
|
+
aria-label={meta.label}
|
|
136
|
+
className="rounded-sm bg-transparent text-center !font-mono text-h4 tabular-nums text-ods-text-primary outline-none placeholder:text-ods-text-secondary focus:bg-ods-bg-hover"
|
|
137
|
+
/>
|
|
138
|
+
</Fragment>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -9,6 +9,7 @@ export * from './checkbox'
|
|
|
9
9
|
export * from './checkbox-block'
|
|
10
10
|
export * from './checkbox-with-description'
|
|
11
11
|
export * from './date-picker'
|
|
12
|
+
export * from './duration-input'
|
|
12
13
|
export * from './field-wrapper'
|
|
13
14
|
export * from './honeypot-field'
|
|
14
15
|
export * from './info-card'
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { MspOrganizationCard } from '../components/chat/msp-organization-card'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MSP Organization card for the AI Assistant welcome screen.
|
|
7
|
+
*
|
|
8
|
+
* Shows which organization the user is signing into — square logo,
|
|
9
|
+
* "Your IT is managed by {name}" with the website beneath, and a trailing
|
|
10
|
+
* external-link button. Mirrors Figma `openframe — fae-chat` (node 1:5540).
|
|
11
|
+
*/
|
|
12
|
+
const meta: Meta<typeof MspOrganizationCard> = {
|
|
13
|
+
title: 'Chat/AI Assistant Welcome Screen/MSP Organization',
|
|
14
|
+
component: MspOrganizationCard,
|
|
15
|
+
parameters: {
|
|
16
|
+
layout: 'centered',
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component:
|
|
20
|
+
'Welcome-screen card confirming the active MSP/organization. The trailing button renders only when `href` (anchor, new tab) or `onOpenWebsite` (callback) is provided.',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
decorators: [
|
|
25
|
+
// The card itself is width-unconstrained (fills its parent). This 600px
|
|
26
|
+
// wrapper mirrors the Figma welcome-screen container (node 1:5540 = 600×80).
|
|
27
|
+
(Story: () => ReactNode) => (
|
|
28
|
+
<div style={{ width: 600, maxWidth: '100%' }}>
|
|
29
|
+
<Story />
|
|
30
|
+
</div>
|
|
31
|
+
),
|
|
32
|
+
],
|
|
33
|
+
args: {
|
|
34
|
+
name: 'TechFlow Solutions',
|
|
35
|
+
website: 'www.techflow.com',
|
|
36
|
+
href: 'https://www.techflow.com',
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default meta
|
|
41
|
+
|
|
42
|
+
type Story = StoryObj<typeof MspOrganizationCard>
|
|
43
|
+
|
|
44
|
+
/** Default — logo image, website, and a link-out anchor (opens in a new tab). */
|
|
45
|
+
export const Default: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
logoUrl:
|
|
48
|
+
'data:image/svg+xml;utf8,' +
|
|
49
|
+
encodeURIComponent(
|
|
50
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><rect width="48" height="48" fill="%231d4ed8"/><circle cx="24" cy="24" r="12" fill="%2360a5fa"/></svg>',
|
|
51
|
+
),
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** No logo — falls back to initials derived from `name`. */
|
|
56
|
+
export const NoLogo: Story = {}
|
|
57
|
+
|
|
58
|
+
/** Callback variant — the button fires `onOpenWebsite` instead of navigating. */
|
|
59
|
+
export const WithCallback: Story = {
|
|
60
|
+
args: {
|
|
61
|
+
href: undefined,
|
|
62
|
+
onOpenWebsite: () => alert('Open organization website'),
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** No action — omit both `href` and `onOpenWebsite` to hide the trailing button. */
|
|
67
|
+
export const WithoutAction: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
href: undefined,
|
|
70
|
+
onOpenWebsite: undefined,
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Long values truncate to a single line each. */
|
|
75
|
+
export const LongValues: Story = {
|
|
76
|
+
args: {
|
|
77
|
+
name: 'Global Managed Services & Infrastructure Partners International',
|
|
78
|
+
website: 'www.global-managed-services-and-infrastructure-partners.example.com',
|
|
79
|
+
},
|
|
80
|
+
}
|