@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.
Files changed (124) hide show
  1. package/dist/{chunk-L5JSGNT3.cjs → chunk-27IO7V3P.cjs} +9 -9
  2. package/dist/{chunk-L5JSGNT3.cjs.map → chunk-27IO7V3P.cjs.map} +1 -1
  3. package/dist/{chunk-ZYGVJXJ5.cjs → chunk-2SIG5WZQ.cjs} +19 -19
  4. package/dist/{chunk-ZYGVJXJ5.cjs.map → chunk-2SIG5WZQ.cjs.map} +1 -1
  5. package/dist/{chunk-63A53WQN.cjs → chunk-3CW4QIUC.cjs} +29 -29
  6. package/dist/{chunk-63A53WQN.cjs.map → chunk-3CW4QIUC.cjs.map} +1 -1
  7. package/dist/{chunk-GHVVOST5.js → chunk-4APKC3JB.js} +5 -5
  8. package/dist/{chunk-OLEW7FYZ.cjs → chunk-4FX56H4X.cjs} +39 -39
  9. package/dist/{chunk-OLEW7FYZ.cjs.map → chunk-4FX56H4X.cjs.map} +1 -1
  10. package/dist/{chunk-J7AV6H63.js → chunk-4MD2TPFY.js} +75 -75
  11. package/dist/{chunk-JEBL5PQK.js → chunk-5EV3GLZK.js} +4 -4
  12. package/dist/{chunk-6KERXOFE.js → chunk-5EZ3WDDL.js} +3 -3
  13. package/dist/{chunk-64DZ2J7Q.js → chunk-6W3MPA7N.js} +3 -3
  14. package/dist/{chunk-Q4AMYLKX.js → chunk-7KZJV4MP.js} +2 -2
  15. package/dist/{chunk-JAZM3A7E.js → chunk-A6NBGRGZ.js} +2 -2
  16. package/dist/{chunk-PIJ4JLJU.js → chunk-ALP347PU.js} +6 -6
  17. package/dist/{chunk-CUNMBP3A.cjs → chunk-B43HTMWN.cjs} +13 -13
  18. package/dist/{chunk-CUNMBP3A.cjs.map → chunk-B43HTMWN.cjs.map} +1 -1
  19. package/dist/{chunk-4MCMPYEM.cjs → chunk-BMCO4Z6Y.cjs} +12 -12
  20. package/dist/{chunk-4MCMPYEM.cjs.map → chunk-BMCO4Z6Y.cjs.map} +1 -1
  21. package/dist/{chunk-VCE3ZEN3.cjs → chunk-EBL2SQ6P.cjs} +5 -5
  22. package/dist/{chunk-VCE3ZEN3.cjs.map → chunk-EBL2SQ6P.cjs.map} +1 -1
  23. package/dist/{chunk-FCEVVNWY.cjs → chunk-EQTU5KRP.cjs} +73 -73
  24. package/dist/{chunk-FCEVVNWY.cjs.map → chunk-EQTU5KRP.cjs.map} +1 -1
  25. package/dist/{chunk-2Y4DLBFO.js → chunk-FVXOKJ4B.js} +5087 -4887
  26. package/dist/chunk-FVXOKJ4B.js.map +1 -0
  27. package/dist/{chunk-ZS2SBWBR.cjs → chunk-GXRWE7NB.cjs} +2 -2
  28. package/dist/{chunk-ZS2SBWBR.cjs.map → chunk-GXRWE7NB.cjs.map} +1 -1
  29. package/dist/{chunk-XAQJ4ZLY.cjs → chunk-IFG6RJ5S.cjs} +1098 -898
  30. package/dist/chunk-IFG6RJ5S.cjs.map +1 -0
  31. package/dist/{chunk-DHVL36CA.cjs → chunk-JCTNHTPT.cjs} +40 -40
  32. package/dist/{chunk-DHVL36CA.cjs.map → chunk-JCTNHTPT.cjs.map} +1 -1
  33. package/dist/{chunk-CSLMCBZV.js → chunk-JSX6QQ7Z.js} +6 -6
  34. package/dist/{chunk-UGDGUO26.cjs → chunk-K5HBVPJ5.cjs} +3 -3
  35. package/dist/{chunk-UGDGUO26.cjs.map → chunk-K5HBVPJ5.cjs.map} +1 -1
  36. package/dist/{chunk-M3NULYCR.js → chunk-LDBS7C7H.js} +5 -5
  37. package/dist/{chunk-Z3YORGG4.js → chunk-OG6COCMK.js} +2 -2
  38. package/dist/{chunk-FOVX3W3C.cjs → chunk-OY5ZZLAN.cjs} +70 -70
  39. package/dist/{chunk-FOVX3W3C.cjs.map → chunk-OY5ZZLAN.cjs.map} +1 -1
  40. package/dist/{chunk-4V3TCOFC.cjs → chunk-PU22RRNK.cjs} +31 -31
  41. package/dist/{chunk-4V3TCOFC.cjs.map → chunk-PU22RRNK.cjs.map} +1 -1
  42. package/dist/{chunk-YFGDZFUG.js → chunk-ULEUTOOA.js} +3 -3
  43. package/dist/{chunk-LAMDFGE3.cjs → chunk-XHBX7LG2.cjs} +15 -15
  44. package/dist/{chunk-LAMDFGE3.cjs.map → chunk-XHBX7LG2.cjs.map} +1 -1
  45. package/dist/{chunk-4NVA6W3J.js → chunk-XHPONEBL.js} +2 -2
  46. package/dist/{chunk-OKGZK6TT.js → chunk-ZARQHUES.js} +3 -3
  47. package/dist/components/case-studies/index.cjs +10 -10
  48. package/dist/components/case-studies/index.js +4 -4
  49. package/dist/components/chat/index.cjs +6 -4
  50. package/dist/components/chat/index.cjs.map +1 -1
  51. package/dist/components/chat/index.d.ts +1 -0
  52. package/dist/components/chat/index.d.ts.map +1 -1
  53. package/dist/components/chat/index.js +5 -3
  54. package/dist/components/chat/msp-organization-card.d.ts +33 -0
  55. package/dist/components/chat/msp-organization-card.d.ts.map +1 -0
  56. package/dist/components/contact/index.cjs +5 -5
  57. package/dist/components/contact/index.js +4 -4
  58. package/dist/components/docs/index.cjs +7 -7
  59. package/dist/components/docs/index.js +6 -6
  60. package/dist/components/embeds/index.cjs +5 -5
  61. package/dist/components/embeds/index.js +4 -4
  62. package/dist/components/faq/index.cjs +6 -6
  63. package/dist/components/faq/index.js +5 -5
  64. package/dist/components/features/index.cjs +4 -4
  65. package/dist/components/features/index.js +3 -3
  66. package/dist/components/features/time-tracker/time-tracker-header-button.d.ts.map +1 -1
  67. package/dist/components/features/time-tracker/time-tracker-panel.d.ts.map +1 -1
  68. package/dist/components/help-center-pages/index.cjs +23 -23
  69. package/dist/components/help-center-pages/index.js +14 -14
  70. package/dist/components/icons-v2-generated/index.cjs +2 -2
  71. package/dist/components/icons-v2-generated/index.js +1 -1
  72. package/dist/components/index.cjs +129 -125
  73. package/dist/components/index.cjs.map +1 -1
  74. package/dist/components/index.js +16 -12
  75. package/dist/components/index.js.map +1 -1
  76. package/dist/components/navigation/app-header.d.ts.map +1 -1
  77. package/dist/components/navigation/index.cjs +4 -4
  78. package/dist/components/navigation/index.js +3 -3
  79. package/dist/components/onboarding-guides/index.cjs +8 -8
  80. package/dist/components/onboarding-guides/index.js +7 -7
  81. package/dist/components/related-content/index.cjs +6 -6
  82. package/dist/components/related-content/index.js +5 -5
  83. package/dist/components/tickets/index.cjs +7 -7
  84. package/dist/components/tickets/index.js +6 -6
  85. package/dist/components/ui/date-picker.d.ts.map +1 -1
  86. package/dist/components/ui/duration-input.d.ts +14 -0
  87. package/dist/components/ui/duration-input.d.ts.map +1 -0
  88. package/dist/components/ui/index.cjs +8 -4
  89. package/dist/components/ui/index.cjs.map +1 -1
  90. package/dist/components/ui/index.d.ts +1 -0
  91. package/dist/components/ui/index.d.ts.map +1 -1
  92. package/dist/components/ui/index.js +7 -3
  93. package/dist/hooks/index.cjs +3 -3
  94. package/dist/hooks/index.js +2 -2
  95. package/dist/hooks/use-toast.d.ts +1 -1
  96. package/dist/index.cjs +8 -4
  97. package/dist/index.cjs.map +1 -1
  98. package/dist/index.js +7 -3
  99. package/package.json +1 -1
  100. package/src/components/chat/index.ts +1 -0
  101. package/src/components/chat/msp-organization-card.tsx +81 -0
  102. package/src/components/features/time-tracker/time-tracker-header-button.tsx +2 -1
  103. package/src/components/features/time-tracker/time-tracker-panel.tsx +10 -4
  104. package/src/components/navigation/app-header.tsx +4 -1
  105. package/src/components/ui/date-picker.tsx +56 -16
  106. package/src/components/ui/duration-input.tsx +142 -0
  107. package/src/components/ui/index.ts +1 -0
  108. package/src/stories/MspOrganizationCard.stories.tsx +80 -0
  109. package/dist/chunk-2Y4DLBFO.js.map +0 -1
  110. package/dist/chunk-XAQJ4ZLY.cjs.map +0 -1
  111. /package/dist/{chunk-GHVVOST5.js.map → chunk-4APKC3JB.js.map} +0 -0
  112. /package/dist/{chunk-J7AV6H63.js.map → chunk-4MD2TPFY.js.map} +0 -0
  113. /package/dist/{chunk-JEBL5PQK.js.map → chunk-5EV3GLZK.js.map} +0 -0
  114. /package/dist/{chunk-6KERXOFE.js.map → chunk-5EZ3WDDL.js.map} +0 -0
  115. /package/dist/{chunk-64DZ2J7Q.js.map → chunk-6W3MPA7N.js.map} +0 -0
  116. /package/dist/{chunk-Q4AMYLKX.js.map → chunk-7KZJV4MP.js.map} +0 -0
  117. /package/dist/{chunk-JAZM3A7E.js.map → chunk-A6NBGRGZ.js.map} +0 -0
  118. /package/dist/{chunk-PIJ4JLJU.js.map → chunk-ALP347PU.js.map} +0 -0
  119. /package/dist/{chunk-CSLMCBZV.js.map → chunk-JSX6QQ7Z.js.map} +0 -0
  120. /package/dist/{chunk-M3NULYCR.js.map → chunk-LDBS7C7H.js.map} +0 -0
  121. /package/dist/{chunk-Z3YORGG4.js.map → chunk-OG6COCMK.js.map} +0 -0
  122. /package/dist/{chunk-YFGDZFUG.js.map → chunk-ULEUTOOA.js.map} +0 -0
  123. /package/dist/{chunk-4NVA6W3J.js.map → chunk-XHPONEBL.js.map} +0 -0
  124. /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={onCancel}
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="flex w-full flex-wrap gap-[var(--spacing-system-m)]">
216
+ <div className="grid grid-cols-2 gap-[var(--spacing-system-m)]">
213
217
  <Button
214
218
  variant="outline"
215
- className="min-w-0 flex-1"
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={dimmedClass}
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
- // Check if we have a complete range (both from and to, and they're different)
137
- const rangeSelected = selected as DateRange | undefined;
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 className="bg-ods-card border border-ods-border rounded-[6px] overflow-hidden">
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
- {numberOfMonths === 1 && (
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
- {numberOfMonths === 2 && <div className="size-10 md:size-12" />}
354
+ {monthsToShow === 2 && <div className="size-10 md:size-12" />}
319
355
  </div>
320
356
  <DayPicker
321
357
  mode="range"
322
- selected={selected as DateRange | undefined}
323
- onSelect={(range) => onSelect(range)}
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 numberOfMonths === 2) */}
337
- {numberOfMonths === 2 && (
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={selected as DateRange | undefined}
353
- onSelect={(range) => onSelect(range)}
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
- // Only close popover when BOTH from and to dates are selected
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
+ }