@flamingo-stack/openframe-frontend-core 0.0.715 → 0.0.717

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 (110) hide show
  1. package/dist/chat-protocol/index.cjs +23 -14
  2. package/dist/chat-protocol/index.js +23 -14
  3. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
  4. package/dist/chat-protocol/source-metadata.d.ts +11 -2
  5. package/dist/chat-protocol/source-metadata.d.ts.map +1 -1
  6. package/dist/{chunk-WEEZSGFD.cjs → chunk-2AI2E3O2.cjs} +65 -65
  7. package/dist/{chunk-KSEOIICU.js → chunk-2EK7PA73.js} +3 -3
  8. package/dist/{chunk-AQI74KC7.js → chunk-2X2P3VHF.js} +5 -5
  9. package/dist/{chunk-4GFAKUHF.js → chunk-3MJPCKB6.js} +2 -2
  10. package/dist/{chunk-W4CS6UJS.cjs → chunk-3QAMMWV2.cjs} +8 -8
  11. package/dist/{chunk-JHSPIDV5.js → chunk-47FISYO5.js} +3 -3
  12. package/dist/{chunk-ZSJ6WWFH.cjs → chunk-5MF3NMYC.cjs} +451 -392
  13. package/dist/{chunk-VKIRTB7K.js → chunk-6MZI5SQ4.js} +2 -2
  14. package/dist/{chunk-2LZ3IYGT.js → chunk-B5XU6KOA.js} +2269 -2210
  15. package/dist/{chunk-QW7KTWLB.js → chunk-E6FLX4TX.js} +3 -3
  16. package/dist/{chunk-ZX7SCPLO.cjs → chunk-E7YCPY7K.cjs} +3 -3
  17. package/dist/{chunk-BIFUUH4K.js → chunk-FGKSAQR3.js} +5 -5
  18. package/dist/{chunk-EOKSKJSS.cjs → chunk-FROXS52D.cjs} +11 -11
  19. package/dist/{chunk-GXHWSAV6.cjs → chunk-J3Q53TWW.cjs} +26 -26
  20. package/dist/{chunk-ROBQ5TR7.js → chunk-JH2SIWPQ.js} +28 -17
  21. package/dist/{chunk-DDWFCWOE.js → chunk-JPD4NXH6.js} +1 -1
  22. package/dist/{chunk-LTLHCAZY.cjs → chunk-LANNTTKP.cjs} +9 -9
  23. package/dist/{chunk-JVCAK4W3.cjs → chunk-LE5WKGJV.cjs} +3 -3
  24. package/dist/{chunk-XFFIUXVI.cjs → chunk-M2A5SC6H.cjs} +9 -9
  25. package/dist/{chunk-5MMCYJAJ.cjs → chunk-NB6EWUDE.cjs} +33 -33
  26. package/dist/{chunk-H5R7U2D2.js → chunk-NZ5BHDFZ.js} +1 -1
  27. package/dist/{chunk-E25A235W.cjs → chunk-OHLBO4LM.cjs} +3 -3
  28. package/dist/{chunk-DI5VLY3G.js → chunk-PD4W2VMH.js} +1 -1
  29. package/dist/{chunk-OQVGA3YE.js → chunk-TYVS64YN.js} +2 -2
  30. package/dist/{chunk-EORJFB2Z.cjs → chunk-U3TP4UGA.cjs} +6 -6
  31. package/dist/{chunk-HETQETPV.cjs → chunk-UWQWMGJ7.cjs} +28 -17
  32. package/dist/{chunk-U4WCFIYB.cjs → chunk-VYKFM2NB.cjs} +35 -35
  33. package/dist/{chunk-YBS5QGON.js → chunk-VZI5BA4L.js} +1 -1
  34. package/dist/{chunk-VBDGMXZO.js → chunk-WH5GJSID.js} +2 -2
  35. package/dist/{chunk-S66W6HNZ.cjs → chunk-X3RVDBXU.cjs} +16 -16
  36. package/dist/{cloud-tenants-MI7APZJD.cjs → cloud-tenants-KDWTVHSI.cjs} +3 -3
  37. package/dist/{cloud-tenants-NSEVWC26.js → cloud-tenants-LNC26WY2.js} +2 -2
  38. package/dist/components/case-studies/index.cjs +14 -14
  39. package/dist/components/case-studies/index.js +3 -3
  40. package/dist/components/chat/index.cjs +2 -2
  41. package/dist/components/chat/index.js +1 -1
  42. package/dist/components/chat/types/message.types.d.ts +20 -16
  43. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  44. package/dist/components/chat/types/unified-chat-state.types.d.ts +1 -1
  45. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  46. package/dist/components/contact/index.cjs +4 -4
  47. package/dist/components/contact/index.js +3 -3
  48. package/dist/components/docs/index.cjs +7 -7
  49. package/dist/components/docs/index.js +6 -6
  50. package/dist/components/embeds/index.cjs +4 -4
  51. package/dist/components/embeds/index.js +3 -3
  52. package/dist/components/faq/index.cjs +4 -4
  53. package/dist/components/faq/index.js +3 -3
  54. package/dist/components/features/approval-level.d.ts +30 -22
  55. package/dist/components/features/approval-level.d.ts.map +1 -1
  56. package/dist/components/features/index.cjs +3 -5
  57. package/dist/components/features/index.js +4 -6
  58. package/dist/components/help-center-pages/index.cjs +62 -62
  59. package/dist/components/help-center-pages/index.js +13 -13
  60. package/dist/components/index.cjs +33 -33
  61. package/dist/components/index.js +16 -16
  62. package/dist/components/meeting-scheduler/index.cjs +19 -19
  63. package/dist/components/meeting-scheduler/index.js +2 -2
  64. package/dist/components/navigation/index.cjs +6 -6
  65. package/dist/components/navigation/index.js +5 -5
  66. package/dist/components/onboarding-guides/index.cjs +5 -5
  67. package/dist/components/onboarding-guides/index.js +4 -4
  68. package/dist/components/product-demo/index.cjs +6 -6
  69. package/dist/components/product-demo/index.js +6 -6
  70. package/dist/components/tickets/index.cjs +7 -7
  71. package/dist/components/tickets/index.js +6 -6
  72. package/dist/components/ui/analyzing-state.d.ts +17 -0
  73. package/dist/components/ui/analyzing-state.d.ts.map +1 -0
  74. package/dist/components/ui/feature-list.d.ts +4 -4
  75. package/dist/components/ui/feature-list.d.ts.map +1 -1
  76. package/dist/components/ui/incident-feed.d.ts +4 -1
  77. package/dist/components/ui/incident-feed.d.ts.map +1 -1
  78. package/dist/components/ui/index.cjs +5 -3
  79. package/dist/components/ui/index.d.ts +1 -0
  80. package/dist/components/ui/index.d.ts.map +1 -1
  81. package/dist/components/ui/index.js +4 -2
  82. package/dist/components/ui/playback-toggle.d.ts +10 -4
  83. package/dist/components/ui/playback-toggle.d.ts.map +1 -1
  84. package/dist/components/ui/tab-navigation.d.ts +7 -1
  85. package/dist/components/ui/tab-navigation.d.ts.map +1 -1
  86. package/dist/{devices-6NMK4EXV.cjs → devices-ATHYJUVQ.cjs} +4 -4
  87. package/dist/{devices-W3VKQEVZ.js → devices-NYCRQ3PG.js} +2 -2
  88. package/dist/index.cjs +7 -7
  89. package/dist/index.js +10 -10
  90. package/dist/{knowledge-YWCB7HNC.js → knowledge-DEKYJXBS.js} +2 -2
  91. package/dist/{knowledge-GDMGHFEC.cjs → knowledge-EHNRYT7U.cjs} +4 -4
  92. package/dist/{logs-7QDRV3GN.cjs → logs-3V765P7O.cjs} +11 -11
  93. package/dist/{logs-E742S5A2.js → logs-Q7YFIVYI.js} +2 -2
  94. package/dist/{policies-XTPAKRHI.js → policies-BTIENK5A.js} +2 -2
  95. package/dist/{policies-BEVF6AEM.cjs → policies-TXU5AIXS.cjs} +7 -7
  96. package/dist/{software-update-KPHX4XQ7.cjs → software-update-MINTICZY.cjs} +7 -7
  97. package/dist/{software-update-FXWIEKSW.js → software-update-SW2HOBHW.js} +2 -2
  98. package/package.json +1 -1
  99. package/src/chat-protocol/nats-decoder.ts +10 -8
  100. package/src/chat-protocol/source-metadata.ts +25 -2
  101. package/src/components/chat/types/message.types.ts +21 -15
  102. package/src/components/chat/types/unified-chat-state.types.ts +1 -1
  103. package/src/components/chat/utils/process-historical-messages.ts +12 -4
  104. package/src/components/features/approval-level.tsx +62 -44
  105. package/src/components/ui/analyzing-state.tsx +57 -0
  106. package/src/components/ui/feature-list.tsx +26 -16
  107. package/src/components/ui/incident-feed.tsx +6 -2
  108. package/src/components/ui/index.ts +1 -0
  109. package/src/components/ui/playback-toggle.tsx +12 -6
  110. package/src/components/ui/tab-navigation.tsx +12 -1
@@ -30,8 +30,8 @@ export interface ApprovalLevelMeta {
30
30
 
31
31
  /**
32
32
  * THE look and wording of an approval level: its name, its one-line meaning,
33
- * its icon and its colour. The guardrails panel's dropdown, the icon control
34
- * below and any read-only mark read it, so a level is the same everywhere.
33
+ * its icon and its colour. The guardrails panel's dropdown and `ApprovalLevelView`
34
+ * below read it, so a level is the same everywhere.
35
35
  */
36
36
  export const APPROVAL_LEVEL_META: Record<ApprovalLevel, ApprovalLevelMeta> = {
37
37
  ALLOW: {
@@ -57,61 +57,79 @@ export const APPROVAL_LEVEL_META: Record<ApprovalLevel, ApprovalLevelMeta> = {
57
57
 
58
58
  const ICON_PX = 16;
59
59
 
60
- export interface ApprovalLevelMarkProps {
61
- level: ApprovalLevel;
62
- /** Icon only: the name is the mark's accessible name and its tooltip. Default false (icon and name). */
63
- iconOnly?: boolean;
64
- className?: string;
65
- }
66
-
67
- /** One level, read-only: its icon in its colour, with its name beside it or as its tooltip. */
68
- export function ApprovalLevelMark({ level, iconOnly = false, className }: ApprovalLevelMarkProps) {
69
- const { label, hint, Icon, toneClassName } = APPROVAL_LEVEL_META[level];
70
- const mark = (
71
- <span className={cn('inline-flex items-center gap-[var(--spacing-system-xxs)] text-h6', toneClassName, className)}>
72
- <Icon size={ICON_PX} className="shrink-0" aria-hidden />
73
- {iconOnly ? <span className="sr-only">{label}</span> : label}
74
- </span>
75
- );
76
- return iconOnly ? (
77
- <TouchFriendlyTooltip content={`${label}: ${hint}`} side="top">
78
- {mark}
79
- </TouchFriendlyTooltip>
80
- ) : (
81
- mark
82
- );
83
- }
84
-
85
- export interface ApprovalLevelControlProps {
60
+ export interface ApprovalLevelViewProps {
61
+ /** The level in force. */
86
62
  value: ApprovalLevel;
87
- onChange: (level: ApprovalLevel) => void;
88
- /** The id of the element that names what is being decided (the rule's label). */
89
- 'aria-labelledby'?: string;
90
- /** The level in force, written beside the icons. Default true: a row reads without hovering, and on touch. */
91
- showLabel?: boolean;
63
+ /**
64
+ * Whether the level can be changed here. `false`: the level's own icon.
65
+ * `true`: four icons, one per level, in a single-choice group. Default: true
66
+ * when `onChange` is given.
67
+ */
68
+ editable?: boolean;
69
+ /** Editable: the level picked. */
70
+ onChange?: (level: ApprovalLevel) => void;
71
+ /** Editable: the group is shown and cannot be changed (a locked rule). */
92
72
  disabled?: boolean;
73
+ /**
74
+ * The level's name, written before the icons. Default true: a row reads
75
+ * without hovering, and on touch. False keeps the name as the accessible
76
+ * name (and, read-only, as the icon's tooltip).
77
+ */
78
+ showLabel?: boolean;
79
+ /** Editable: the id of the element that names what is being decided (the rule's label). */
80
+ 'aria-labelledby'?: string;
93
81
  className?: string;
94
82
  }
95
83
 
96
84
  /**
97
- * A choice of approval level as four icons, one per level, in a single-choice
98
- * group (arrow keys move, like every toggle group). Each icon has the level's
99
- * name as its accessible name and a tooltip with what the level means (hover
100
- * or keyboard focus; a tap selects). The level in force wears its colour, and its name
101
- * is written beside the group, so the choice never depends on a tooltip.
85
+ * THE way a level is shown, read-only or editable, by one prop: the level's
86
+ * name in its colour, then its icon (read-only) or the four levels' icons as a
87
+ * single-choice group (editable; arrow keys move, like every toggle group).
88
+ * Name, icon, colour and tooltip are `APPROVAL_LEVEL_META`'s in both, so a
89
+ * rule reads the same on a page that shows it and on one that lets it change.
90
+ *
91
+ * Editable, each icon has its level's name as its accessible name and a
92
+ * tooltip with what the level means (hover or keyboard focus; a tap selects).
102
93
  */
103
- export function ApprovalLevelControl({
94
+ export function ApprovalLevelView({
104
95
  value,
96
+ editable,
105
97
  onChange,
106
- 'aria-labelledby': labelledBy,
107
- showLabel = true,
108
98
  disabled = false,
99
+ showLabel = true,
100
+ 'aria-labelledby': labelledBy,
109
101
  className,
110
- }: ApprovalLevelControlProps) {
102
+ }: ApprovalLevelViewProps) {
111
103
  const current = APPROVAL_LEVEL_META[value];
104
+ const canEdit = editable ?? onChange !== undefined;
105
+ const name = showLabel ? (
106
+ <span className={cn('whitespace-nowrap text-h6', current.toneClassName)}>{current.label}</span>
107
+ ) : null;
108
+
109
+ if (!canEdit) {
110
+ const icon = (
111
+ <span className={cn('inline-flex shrink-0', current.toneClassName)}>
112
+ <current.Icon size={ICON_PX} aria-hidden />
113
+ {!showLabel && <span className="sr-only">{current.label}</span>}
114
+ </span>
115
+ );
116
+ return (
117
+ <span className={cn('inline-flex shrink-0 items-center gap-[var(--spacing-system-xxs)]', className)}>
118
+ {name}
119
+ {showLabel ? (
120
+ icon
121
+ ) : (
122
+ <TouchFriendlyTooltip content={`${current.label}: ${current.hint}`} side="top">
123
+ {icon}
124
+ </TouchFriendlyTooltip>
125
+ )}
126
+ </span>
127
+ );
128
+ }
129
+
112
130
  return (
113
131
  <div className={cn('flex shrink-0 items-center gap-[var(--spacing-system-sf)]', className)}>
114
- {showLabel && <span className={cn('whitespace-nowrap text-h6', current.toneClassName)}>{current.label}</span>}
132
+ {name}
115
133
  <TooltipProvider delayDuration={150}>
116
134
  <ToggleGroup
117
135
  type="single"
@@ -121,7 +139,7 @@ export function ApprovalLevelControl({
121
139
  disabled={disabled}
122
140
  // Clicking the level in force reports '' (deselect): a rule always has a level.
123
141
  onValueChange={(next: string) => {
124
- if (next && next !== value) onChange(next as ApprovalLevel);
142
+ if (next && next !== value) onChange?.(next as ApprovalLevel);
125
143
  }}
126
144
  aria-labelledby={labelledBy}
127
145
  >
@@ -0,0 +1,57 @@
1
+ import { cn } from '../../utils/cn';
2
+ import { ChatTypingIndicator } from '../chat/chat-typing-indicator';
3
+ import { Skeleton } from './skeleton';
4
+
5
+ export interface AnalyzingStateProps {
6
+ /** What is being worked on, as a sentence with no closing mark ("Mingo is analyzing your logs"). */
7
+ label: string;
8
+ className?: string;
9
+ }
10
+
11
+ /** How far each row being read has faded: the list trails off instead of ending. */
12
+ const ROW_OPACITY = ['opacity-100', 'opacity-60', 'opacity-30'] as const;
13
+ /** Each row pulses a beat after the one above it, so the eye reads them as a scan. */
14
+ const ROW_STAGGER_MS = 200;
15
+
16
+ /**
17
+ * THE "it is working, nothing to show yet" state of a feed or a window: three
18
+ * rows still being read, pulsing one after another, over the caller's words
19
+ * and the chat's own typing dots. It says an agent is at work, where a bare
20
+ * sentence in an empty box reads as idle.
21
+ *
22
+ * Built from the shared pieces only: `Skeleton` for the rows and
23
+ * `ChatTypingIndicator` for the dots. Still under reduced motion. It is
24
+ * announced as a status; the rows and dots are decoration.
25
+ */
26
+ export function AnalyzingState({ label, className }: AnalyzingStateProps) {
27
+ return (
28
+ <div role="status" className={cn('flex w-full max-w-sm flex-col items-center gap-4', className)}>
29
+ <div aria-hidden className="grid w-full grid-cols-1 gap-2">
30
+ {ROW_OPACITY.map((opacity, row) => {
31
+ const delay = { animationDelay: `${row * ROW_STAGGER_MS}ms` };
32
+ return (
33
+ <div
34
+ key={opacity}
35
+ className={cn(
36
+ 'flex items-center gap-3 rounded-md border border-ods-border bg-ods-bg px-3.5 py-3',
37
+ opacity,
38
+ )}
39
+ >
40
+ <Skeleton
41
+ className="h-2 w-2 shrink-0 rounded-full bg-ods-flamingo-cyan motion-reduce:animate-none"
42
+ style={delay}
43
+ />
44
+ <Skeleton className="h-2.5 min-w-0 flex-1 motion-reduce:animate-none" style={delay} />
45
+ <Skeleton className="h-2.5 w-12 shrink-0 motion-reduce:animate-none" style={delay} />
46
+ </div>
47
+ );
48
+ })}
49
+ </div>
50
+ {/* A div, not a paragraph: the typing dots are block elements. */}
51
+ <div className="flex items-center gap-1.5 text-center text-ods-text-secondary text-h4">
52
+ {label}
53
+ <ChatTypingIndicator aria-hidden size="sm" dotClassName="bg-ods-text-secondary motion-reduce:animate-none" />
54
+ </div>
55
+ </div>
56
+ );
57
+ }
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import type React from 'react';
4
+ import { Button } from './button';
4
5
 
5
6
  export interface FeatureListItemData {
6
7
  /** Shown in the boxed slot. Not used by the `numbered` variant. */
@@ -15,13 +16,13 @@ export interface FeatureListProps {
15
16
  iconBoxSize?: number;
16
17
  /**
17
18
  * `boxed` (default): one bordered card, a boxed icon per row.
18
- * `numbered`: an ordered list of open rows separated by hairlines, each led
19
- * by its two-digit number (process steps).
19
+ * `numbered`: an ordered list of open rows, each led by its number in a
20
+ * round marker (process steps).
20
21
  */
21
22
  variant?: 'boxed' | 'numbered';
22
23
  /**
23
- * `numbered` only. The step being shown now: it stays at full strength and
24
- * the others step back, so the list reads as "you are here" beside a timed
24
+ * `numbered` only. The step being shown now: it stays at full strength with
25
+ * its marker lit and the others step back, so the list reads as "you are here" beside a timed
25
26
  * demo. Omit for a plain list where every step is equal.
26
27
  */
27
28
  activeIndex?: number;
@@ -43,38 +44,47 @@ export function FeatureList({
43
44
  if (variant === 'numbered') {
44
45
  const dims = typeof activeIndex === 'number';
45
46
  return (
46
- <ol className={`flex flex-col ${className}`}>
47
+ <ol className={`flex flex-col gap-[var(--spacing-system-lf)] ${className}`}>
47
48
  {items.map((item, index) => {
48
49
  const current = dims && index === activeIndex;
49
50
  const row = (
50
51
  <>
51
- <span className={`text-h5 ${current ? 'text-ods-flamingo-pink' : 'text-ods-text-secondary'}`}>
52
- {String(index + 1).padStart(2, '0')}
52
+ <span
53
+ className={`grid h-7 w-7 shrink-0 place-items-center rounded-full border transition-colors duration-200 text-h5 motion-reduce:transition-none ${
54
+ current
55
+ ? 'border-ods-flamingo-pink bg-ods-flamingo-pink-secondary text-ods-text-primary'
56
+ : 'border-ods-border text-ods-text-secondary'
57
+ }`}
58
+ >
59
+ {index + 1}
53
60
  </span>
54
61
  <div className="flex min-w-0 flex-col gap-[var(--spacing-system-xxs)]">
55
- <h3 className="text-ods-text-primary text-h3">{item.title}</h3>
56
- <p className="text-ods-text-secondary text-h4">{item.description}</p>
62
+ <h3 className="text-ods-text-primary text-h4">{item.title}</h3>
63
+ <p className="text-ods-text-secondary text-h6">{item.description}</p>
57
64
  </div>
58
65
  </>
59
66
  );
60
- const layout =
61
- 'grid grid-cols-[48px_minmax(0,1fr)] gap-[var(--spacing-system-m)] py-[var(--spacing-system-l)]';
67
+ const layout = 'flex items-start gap-[var(--spacing-system-sf)]';
62
68
  return (
63
69
  <li
64
70
  key={index}
65
71
  aria-current={current ? 'step' : undefined}
66
- className={`border-t border-ods-border transition-opacity duration-200 last:border-b motion-reduce:transition-none ${
72
+ className={`transition-opacity duration-200 motion-reduce:transition-none ${
67
73
  dims && !current ? 'opacity-55 focus-within:opacity-100 hover:opacity-100' : ''
68
74
  }`}
69
75
  >
70
76
  {onSelect ? (
71
- <button
72
- type="button"
77
+ // The unified Button with no box of its own (`wrap`): it brings the
78
+ // target, the focus ring and the keyboard semantics; the row is the look.
79
+ <Button
80
+ variant="transparent"
81
+ size="wrap"
82
+ fullWidth
73
83
  onClick={() => onSelect(index)}
74
- className={`${layout} w-full cursor-pointer text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus`}
84
+ className={`${layout} justify-start whitespace-normal text-left hover:bg-transparent active:bg-transparent`}
75
85
  >
76
86
  {row}
77
- </button>
87
+ </Button>
78
88
  ) : (
79
89
  <div className={layout}>{row}</div>
80
90
  )}
@@ -1,5 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { cn } from '../../utils/cn';
3
+ import { AnalyzingState } from './analyzing-state';
3
4
  import { StatusLine, type StatusLineTone } from './status-line';
4
5
 
5
6
  /** Where one incident stands. `waiting` is the only state that needs a person. */
@@ -46,7 +47,10 @@ export interface IncidentFeedProps {
46
47
  /** Oldest first. The newest is open; earlier ones collapse to a row. */
47
48
  items: readonly IncidentFeedItem[];
48
49
  statusLabels: Record<IncidentStatus, string>;
49
- /** Shown while there is nothing in the feed. */
50
+ /**
51
+ * Shown while there is nothing in the feed, under rows that are still being
52
+ * read: the agent is at work, it has just not found anything yet.
53
+ */
50
54
  emptyLabel?: string;
51
55
  /**
52
56
  * `top`: the list grows downward (a wide window). `bottom`: the newest item
@@ -91,7 +95,7 @@ export function IncidentFeed({ items, statusLabels, emptyLabel, anchor = 'top',
91
95
  className,
92
96
  )}
93
97
  >
94
- {items.length === 0 && emptyLabel && <p className="m-auto text-ods-text-secondary text-h4">{emptyLabel}</p>}
98
+ {items.length === 0 && emptyLabel && <AnalyzingState label={emptyLabel} className="m-auto" />}
95
99
  {items.map((item, index) => {
96
100
  const open = index === lastIndex || item.status === 'waiting';
97
101
  if (!open) {
@@ -236,6 +236,7 @@ export { LazyMount, type LazyMountProps } from './lazy-mount';
236
236
  export * from './app-window-frame';
237
237
  export * from './product-screen-frame';
238
238
  export * from './phone-frame';
239
+ export * from './analyzing-state';
239
240
  export * from './incident-feed';
240
241
  export * from './status-line';
241
242
  export * from './snap-carousel';
@@ -17,10 +17,16 @@ export interface PlaybackToggleProps {
17
17
 
18
18
  /**
19
19
  * THE stop/start control of anything that moves on its own (WCAG 2.2.2): one
20
- * small, quiet round icon button. A looping demo and an auto-advancing
21
- * carousel show the same one, wired to `useAutoplay`'s `paused` / `setPaused`.
22
- * Quiet on purpose: it takes the accent only on hover, so it never competes
23
- * with what it controls.
20
+ * small icon button beside what it controls. A looping demo and an
21
+ * auto-advancing carousel show the same one, wired to `useAutoplay`'s
22
+ * `paused` / `setPaused`.
23
+ *
24
+ * It has to be there and easy to reach (a 32px target, a name, a focus ring),
25
+ * never the thing the eye lands on: no border and no surface, only a small
26
+ * grey glyph that takes the accent on hover and focus. The grey is the muted
27
+ * text token, the quietest one that still clears the 3:1 a control's glyph
28
+ * needs against the page (WCAG 1.4.11); the next grey down does not. The
29
+ * target stays 32px; it is the glyph that is small.
24
30
  */
25
31
  export function PlaybackToggle({
26
32
  paused,
@@ -34,14 +40,14 @@ export function PlaybackToggle({
34
40
  variant="transparent"
35
41
  size="icon-sm"
36
42
  className={cn(
37
- 'shrink-0 rounded-full border border-ods-border text-ods-text-secondary hover:border-ods-accent hover:text-ods-accent',
43
+ 'shrink-0 rounded-full border-0 text-ods-text-muted hover:text-ods-accent focus-visible:text-ods-accent [&_svg]:h-3 [&_svg]:w-3',
38
44
  className,
39
45
  )}
40
46
  aria-label={paused ? playLabel : pauseLabel}
41
47
  aria-pressed={paused}
42
48
  onClick={() => onChange(!paused)}
43
49
  >
44
- {paused ? <PlayIcon size={16} /> : <PauseIcon size={16} />}
50
+ {paused ? <PlayIcon size={12} /> : <PauseIcon size={12} />}
45
51
  </Button>
46
52
  );
47
53
  }
@@ -84,6 +84,12 @@ interface TabNavigationProps {
84
84
  /** Tabs grow to share the bar's full width equally (Figma segmented-underline
85
85
  * look, e.g. the 480px homepage strip switcher). Default: natural width. */
86
86
  stretchTabs?: boolean;
87
+ /**
88
+ * Where a tab's content sits inside the tab. `center` (default) suits tabs of
89
+ * natural width; `start` reads better when `stretchTabs` makes each tab wide
90
+ * (a timed demo's tabs: avatar, name and caption from the left edge).
91
+ */
92
+ tabAlign?: 'center' | 'start';
87
93
  /** How long the underline takes to reach a new `TabItem.progress` value. Default 200. */
88
94
  progressTransitionMs?: number;
89
95
 
@@ -130,6 +136,7 @@ interface TabBarProps {
130
136
  showLeftGradient: boolean;
131
137
  showRightGradient: boolean;
132
138
  stretchTabs: boolean;
139
+ tabAlign: 'center' | 'start';
133
140
  progressTransitionMs: number;
134
141
  }
135
142
 
@@ -154,6 +161,7 @@ const TabBar = memo(function TabBarImpl({
154
161
  showLeftGradient,
155
162
  showRightGradient,
156
163
  stretchTabs,
164
+ tabAlign,
157
165
  progressTransitionMs,
158
166
  }: TabBarProps) {
159
167
  const scrollRef = useRef<HTMLDivElement>(null);
@@ -367,7 +375,8 @@ const TabBar = memo(function TabBarImpl({
367
375
  onClick={() => onTabChange(tab.id)}
368
376
  aria-label={tab.badge ? `${tab.label} (${tab.badge})` : undefined}
369
377
  className={cn(
370
- 'relative flex shrink-0 cursor-pointer items-center justify-center gap-[var(--spacing-system-xxs)] p-[var(--spacing-system-m)]',
378
+ 'relative flex shrink-0 cursor-pointer items-center gap-[var(--spacing-system-xxs)] p-[var(--spacing-system-m)]',
379
+ tabAlign === 'start' ? 'justify-start text-left' : 'justify-center',
371
380
  // Named rather than `transition-all`: the only thing that moves
372
381
  // here is colour. `all` also made the browser re-check every
373
382
  // property each frame, including the background GRADIENT — which
@@ -503,6 +512,7 @@ export function TabNavigation({
503
512
  showRightGradient = false,
504
513
  showLeftGradient = false,
505
514
  stretchTabs = false,
515
+ tabAlign = 'center',
506
516
  progressTransitionMs = 200,
507
517
  urlSync = false,
508
518
  defaultTab,
@@ -677,6 +687,7 @@ export function TabNavigation({
677
687
  showLeftGradient={showLeftGradient}
678
688
  showRightGradient={showRightGradient}
679
689
  stretchTabs={stretchTabs}
690
+ tabAlign={tabAlign}
680
691
  progressTransitionMs={progressTransitionMs}
681
692
  />
682
693