@flamingo-stack/openframe-frontend-core 0.0.736 → 0.0.738

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 (189) hide show
  1. package/dist/build/export-map.json +1 -1
  2. package/dist/{chunk-AZL332TK.cjs → chunk-2QMBXGMQ.cjs} +15 -2
  3. package/dist/{chunk-7V2KP2TB.cjs → chunk-2SBHPHLS.cjs} +4 -4
  4. package/dist/{chunk-LIH5OQLY.cjs → chunk-2UZS3ZH7.cjs} +8 -8
  5. package/dist/{chunk-MGW5DO6P.cjs → chunk-3BBOAMWJ.cjs} +7 -7
  6. package/dist/{chunk-HZJRERJW.cjs → chunk-3RFQPQ7Z.cjs} +3 -3
  7. package/dist/{chunk-T27CVQBO.cjs → chunk-4T6ETBL4.cjs} +151 -83
  8. package/dist/{chunk-JM54CNZZ.cjs → chunk-4YQNJJWM.cjs} +95 -95
  9. package/dist/{chunk-UBQSOB3X.cjs → chunk-5BKAUXLR.cjs} +4 -4
  10. package/dist/{chunk-YADG5Q7C.cjs → chunk-63N4FN5V.cjs} +3 -3
  11. package/dist/{chunk-URX2YTCL.cjs → chunk-66R4NGGU.cjs} +77 -71
  12. package/dist/{chunk-EJOXXJJB.cjs → chunk-7F3V45AH.cjs} +29 -29
  13. package/dist/{chunk-BDF3FNOC.cjs → chunk-7GETNFZ5.cjs} +49 -49
  14. package/dist/{chunk-L4ZNR5V7.cjs → chunk-7LTG6CJ3.cjs} +67 -67
  15. package/dist/{chunk-MJGX357L.cjs → chunk-B27QEHYR.cjs} +2 -2
  16. package/dist/{chunk-OV6BGMEL.cjs → chunk-BRKTN6UR.cjs} +340 -328
  17. package/dist/{chunk-VRNNIV5Y.cjs → chunk-EG545WGV.cjs} +4 -4
  18. package/dist/{chunk-ZJNYWNCB.cjs → chunk-FG7HTGT3.cjs} +24 -24
  19. package/dist/{chunk-77SD7RXY.cjs → chunk-GAF4L3JW.cjs} +7 -7
  20. package/dist/{chunk-FTGVG6FW.cjs → chunk-GQZOUVXC.cjs} +34 -34
  21. package/dist/{chunk-ZEX5WU35.cjs → chunk-HNYLTEIS.cjs} +48 -69
  22. package/dist/{chunk-FCGLSWKR.cjs → chunk-IFS4AH77.cjs} +19 -1
  23. package/dist/{chunk-L535OD7V.cjs → chunk-JZNESLJX.cjs} +2 -2
  24. package/dist/{chunk-SZFA2LQR.cjs → chunk-KQ4IKALJ.cjs} +35 -35
  25. package/dist/{chunk-EX2GYIGW.cjs → chunk-LY5VOKV7.cjs} +6 -6
  26. package/dist/{chunk-XLSVQJBA.cjs → chunk-MLKL2YVE.cjs} +44 -44
  27. package/dist/{chunk-BL5GOCHP.cjs → chunk-MTNNYSP3.cjs} +26 -18
  28. package/dist/{chunk-ZRYKDN2J.cjs → chunk-N2CISBJJ.cjs} +15 -15
  29. package/dist/chunk-N5NDPULP.cjs +57 -0
  30. package/dist/{chunk-P6Z3EBPJ.cjs → chunk-R36NZTUJ.cjs} +26 -26
  31. package/dist/{chunk-OJUMVOMP.cjs → chunk-U5BRSRXA.cjs} +923 -759
  32. package/dist/{chunk-DUXLZPMJ.cjs → chunk-UMO55S55.cjs} +2 -2
  33. package/dist/{chunk-JGGSRCRD.cjs → chunk-XGHZIYQN.cjs} +24 -24
  34. package/dist/{chunk-NONQUGRP.cjs → chunk-XMNLPQ75.cjs} +12 -12
  35. package/dist/{cloud-tenants-M55HMW6F.cjs → cloud-tenants-FTQGWXHS.cjs} +16 -16
  36. package/dist/components/agent-identity.d.ts +8 -1
  37. package/dist/components/agent-identity.d.ts.map +1 -1
  38. package/dist/components/agent-identity.js +6 -1
  39. package/dist/components/calendar.cjs +4 -4
  40. package/dist/components/case-studies/index.cjs +28 -28
  41. package/dist/components/chat/chat-turn.d.ts +23 -0
  42. package/dist/components/chat/chat-turn.d.ts.map +1 -0
  43. package/dist/components/chat/chat-turn.js +30 -0
  44. package/dist/components/chat/entity-cards/index.d.ts +1 -0
  45. package/dist/components/chat/entity-cards/index.d.ts.map +1 -1
  46. package/dist/components/chat/entity-cards/index.js +2 -0
  47. package/dist/components/chat/hooks/use-ask-prompts.d.ts +1 -1
  48. package/dist/components/chat/hooks/use-ask-prompts.js +1 -1
  49. package/dist/components/chat/index.cjs +16 -12
  50. package/dist/components/chat/index.d.ts +1 -0
  51. package/dist/components/chat/index.d.ts.map +1 -1
  52. package/dist/components/chat/index.js +1 -0
  53. package/dist/components/chat/mingo-info-card.d.ts +17 -1
  54. package/dist/components/chat/mingo-info-card.d.ts.map +1 -1
  55. package/dist/components/chat/mingo-info-card.js +15 -3
  56. package/dist/components/contact/index.cjs +17 -17
  57. package/dist/components/docs/index.cjs +20 -20
  58. package/dist/components/embeds/index.cjs +17 -17
  59. package/dist/components/faq/faq-ask-card.d.ts +5 -4
  60. package/dist/components/faq/faq-ask-card.d.ts.map +1 -1
  61. package/dist/components/faq/faq-document-page.d.ts +3 -1
  62. package/dist/components/faq/faq-document-page.d.ts.map +1 -1
  63. package/dist/components/faq/faq-document-page.js +2 -0
  64. package/dist/components/faq/faq-section.d.ts +37 -5
  65. package/dist/components/faq/faq-section.d.ts.map +1 -1
  66. package/dist/components/faq/faq-section.js +36 -9
  67. package/dist/components/faq/index.cjs +23 -17
  68. package/dist/components/faq/index.d.ts +1 -1
  69. package/dist/components/faq/index.d.ts.map +1 -1
  70. package/dist/components/faq/index.js +4 -2
  71. package/dist/components/features/auth/auth-benefits-panel.d.ts +3 -5
  72. package/dist/components/features/auth/auth-benefits-panel.d.ts.map +1 -1
  73. package/dist/components/features/auth/auth-benefits-panel.js +3 -19
  74. package/dist/components/features/conversation-card.d.ts +12 -4
  75. package/dist/components/features/conversation-card.d.ts.map +1 -1
  76. package/dist/components/features/conversation-card.js +30 -38
  77. package/dist/components/features/index.cjs +16 -16
  78. package/dist/components/help-center-pages/delivery-page.d.ts +8 -1
  79. package/dist/components/help-center-pages/delivery-page.d.ts.map +1 -1
  80. package/dist/components/help-center-pages/delivery-page.js +16 -4
  81. package/dist/components/help-center-pages/download-apps-page.d.ts +8 -1
  82. package/dist/components/help-center-pages/download-apps-page.d.ts.map +1 -1
  83. package/dist/components/help-center-pages/download-apps-page.js +11 -6
  84. package/dist/components/help-center-pages/index.cjs +216 -161
  85. package/dist/components/help-center-pages/onboarding-guides-catalog-page.d.ts +8 -1
  86. package/dist/components/help-center-pages/onboarding-guides-catalog-page.d.ts.map +1 -1
  87. package/dist/components/help-center-pages/onboarding-guides-catalog-page.js +26 -14
  88. package/dist/components/help-center-pages/product-releases-list-page.d.ts +8 -1
  89. package/dist/components/help-center-pages/product-releases-list-page.d.ts.map +1 -1
  90. package/dist/components/help-center-pages/product-releases-list-page.js +24 -12
  91. package/dist/components/help-center-pages/roadmap-page.d.ts +8 -1
  92. package/dist/components/help-center-pages/roadmap-page.d.ts.map +1 -1
  93. package/dist/components/help-center-pages/roadmap-page.js +25 -13
  94. package/dist/components/index.cjs +103 -79
  95. package/dist/components/meeting-scheduler/index.cjs +49 -49
  96. package/dist/components/navigation/index.cjs +19 -19
  97. package/dist/components/onboarding-guides/index.cjs +19 -19
  98. package/dist/components/platform/ToolBadge.d.ts.map +1 -1
  99. package/dist/components/platform/ToolBadge.js +4 -3
  100. package/dist/components/product-demo/index.cjs +8 -8
  101. package/dist/components/related-content/index.cjs +8 -8
  102. package/dist/components/shared/dev-section/dev-section-page.d.ts +8 -1
  103. package/dist/components/shared/dev-section/dev-section-page.d.ts.map +1 -1
  104. package/dist/components/shared/dev-section/dev-section-page.js +8 -3
  105. package/dist/components/shared/legal-document/legal-document-page.d.ts +8 -1
  106. package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
  107. package/dist/components/shared/legal-document/legal-document-page.js +4 -1
  108. package/dist/components/tickets/index.cjs +22 -21
  109. package/dist/components/ui/app-window-frame.d.ts +7 -1
  110. package/dist/components/ui/app-window-frame.d.ts.map +1 -1
  111. package/dist/components/ui/app-window-frame.js +10 -2
  112. package/dist/components/ui/benefit-list.d.ts +42 -0
  113. package/dist/components/ui/benefit-list.d.ts.map +1 -0
  114. package/dist/components/ui/benefit-list.js +62 -0
  115. package/dist/components/ui/button/button.d.ts +1 -0
  116. package/dist/components/ui/button/button.d.ts.map +1 -1
  117. package/dist/components/ui/button/button.js +15 -2
  118. package/dist/components/ui/dropdown-button.d.ts +20 -2
  119. package/dist/components/ui/dropdown-button.d.ts.map +1 -1
  120. package/dist/components/ui/dropdown-button.js +57 -1
  121. package/dist/components/ui/file-manager/index.cjs +11 -11
  122. package/dist/components/ui/index.cjs +32 -16
  123. package/dist/components/ui/index.d.ts +3 -0
  124. package/dist/components/ui/index.d.ts.map +1 -1
  125. package/dist/components/ui/index.js +3 -0
  126. package/dist/components/ui/leader-row.d.ts +17 -0
  127. package/dist/components/ui/leader-row.d.ts.map +1 -0
  128. package/dist/components/ui/leader-row.js +14 -0
  129. package/dist/components/ui/strip-card.d.ts +23 -0
  130. package/dist/components/ui/strip-card.d.ts.map +1 -0
  131. package/dist/components/ui/strip-card.js +56 -0
  132. package/dist/contexts/assistant-runtime-context.d.ts +5 -5
  133. package/dist/{devices-WWJZFG6E.cjs → devices-MPI5HUQS.cjs} +17 -17
  134. package/dist/{embeddable-chat-76IJ6TQZ.cjs → embeddable-chat-7WLUDBGK.cjs} +84 -84
  135. package/dist/index.cjs +46 -20
  136. package/dist/{knowledge-F4YFQT3G.cjs → knowledge-BS663XXL.cjs} +17 -17
  137. package/dist/{logs-GAYEGEZM.cjs → logs-KS2A2VN5.cjs} +25 -25
  138. package/dist/{policies-ZOVJCJWX.cjs → policies-H3OK6H4Q.cjs} +20 -20
  139. package/dist/{remote-session-Z4YZFG5E.cjs → remote-session-EHKXU4HI.cjs} +7 -7
  140. package/dist/{software-update-H4YE5GMJ.cjs → software-update-HMMMWIL3.cjs} +20 -20
  141. package/dist/{tickets-board-JOSYLOS7.cjs → tickets-board-3YJKOOZE.cjs} +10 -10
  142. package/dist/types/faq.d.ts +17 -0
  143. package/dist/types/faq.d.ts.map +1 -1
  144. package/dist/types/faq.js +4 -0
  145. package/dist/types/index.cjs +4 -0
  146. package/dist/utils/demo-clock.d.ts +22 -0
  147. package/dist/utils/demo-clock.d.ts.map +1 -0
  148. package/dist/utils/demo-clock.js +40 -0
  149. package/dist/utils/identity-tint.d.ts +13 -0
  150. package/dist/utils/identity-tint.d.ts.map +1 -0
  151. package/dist/utils/identity-tint.js +14 -0
  152. package/dist/utils/index.cjs +54 -0
  153. package/dist/utils/index.d.ts +2 -0
  154. package/dist/utils/index.d.ts.map +1 -1
  155. package/dist/utils/index.js +2 -0
  156. package/dist/{video-file-player-6NWGXZ3W.cjs → video-file-player-ZH2ILAPO.cjs} +12 -12
  157. package/package.json +1 -1
  158. package/src/components/agent-identity.tsx +12 -1
  159. package/src/components/chat/chat-turn.tsx +52 -0
  160. package/src/components/chat/entity-cards/index.ts +1 -0
  161. package/src/components/chat/hooks/use-ask-prompts.ts +2 -2
  162. package/src/components/chat/index.ts +1 -0
  163. package/src/components/chat/mingo-info-card.tsx +39 -4
  164. package/src/components/faq/faq-ask-card.tsx +5 -4
  165. package/src/components/faq/faq-document-page.tsx +4 -0
  166. package/src/components/faq/faq-section.tsx +86 -14
  167. package/src/components/faq/index.ts +1 -1
  168. package/src/components/features/auth/auth-benefits-panel.tsx +5 -32
  169. package/src/components/features/conversation-card.tsx +48 -46
  170. package/src/components/help-center-pages/delivery-page.tsx +16 -1
  171. package/src/components/help-center-pages/download-apps-page.tsx +9 -0
  172. package/src/components/help-center-pages/onboarding-guides-catalog-page.tsx +16 -1
  173. package/src/components/help-center-pages/product-releases-list-page.tsx +16 -1
  174. package/src/components/help-center-pages/roadmap-page.tsx +16 -1
  175. package/src/components/platform/ToolBadge.tsx +4 -3
  176. package/src/components/shared/dev-section/dev-section-page.tsx +9 -0
  177. package/src/components/shared/legal-document/legal-document-page.tsx +9 -0
  178. package/src/components/ui/app-window-frame.tsx +18 -2
  179. package/src/components/ui/benefit-list.tsx +97 -0
  180. package/src/components/ui/button/button.tsx +14 -1
  181. package/src/components/ui/dropdown-button.tsx +73 -1
  182. package/src/components/ui/index.ts +3 -0
  183. package/src/components/ui/leader-row.tsx +28 -0
  184. package/src/components/ui/strip-card.tsx +68 -0
  185. package/src/contexts/assistant-runtime-context.tsx +5 -5
  186. package/src/types/faq.ts +18 -0
  187. package/src/utils/demo-clock.ts +62 -0
  188. package/src/utils/identity-tint.ts +23 -0
  189. package/src/utils/index.ts +2 -0
@@ -21,6 +21,7 @@ import type { ComponentType } from 'react';
21
21
  import Link from '../../../embed-shims/next-link';
22
22
  import { useRouter } from '../../../embed-shims/next-navigation';
23
23
  import { formatLegalDate } from '../../../utils/format';
24
+ import { FaqSection, pageFaqProps, type PageFaqSlot } from '../../faq/faq-section';
24
25
  import { PageShell, PageLayout } from '../../ui';
25
26
  import { RichMarkdownRenderer } from '../../ui/markdown';
26
27
  import { useLegalDocs, type LegalDocument } from './use-legal-docs';
@@ -64,6 +65,12 @@ export interface LegalDocumentPageProps {
64
65
  /** Back-button config — same pattern as `DevSectionPage`. Pass `false`
65
66
  * to hide. Default `{ label: 'Back to home', href: '/' }`. */
66
67
  backButton?: { label?: string; href?: string } | false;
68
+ /**
69
+ * The page's own FAQ block: the topic the page is about (stated by the host,
70
+ * never read from its address) and where the host serves the FAQs. Rendered
71
+ * after the page's content, inside its container. Absent: no FAQ block.
72
+ */
73
+ faq?: PageFaqSlot;
67
74
  /** Render the standalone `<PageShell>`. Default true. Pass false when the host
68
75
  * layout already provides the page container — only the padding box renders,
69
76
  * avoiding a nested `<main>`. */
@@ -83,6 +90,7 @@ export function LegalDocumentPage({
83
90
  apiEndpoint,
84
91
  MarkdownRenderer = RichMarkdownRenderer,
85
92
  backButton,
93
+ faq,
86
94
  shell = true,
87
95
  }: LegalDocumentPageProps) {
88
96
  const router = useRouter();
@@ -161,6 +169,7 @@ export function LegalDocumentPage({
161
169
  </div>
162
170
  </div>
163
171
  </div>
172
+ {faq && <FaqSection {...pageFaqProps(faq)} />}
164
173
  </PageLayout>
165
174
  );
166
175
 
@@ -1,5 +1,8 @@
1
+ 'use client';
2
+
1
3
  import type { ReactNode } from 'react';
2
4
  import { cn } from '../../utils/cn';
5
+ import { useAgentTint } from '../agent-identity';
3
6
 
4
7
  export interface AppWindowFrameProps {
5
8
  /** Centered in the title bar. */
@@ -7,6 +10,12 @@ export interface AppWindowFrameProps {
7
10
  children: ReactNode;
8
11
  /** A bar pinned under the body (a status line, an action). */
9
12
  footer?: ReactNode;
13
+ /**
14
+ * The slug of the agent whose window this is. The window takes that agent's
15
+ * tint from its identity, like every card of the agent's. A window names its
16
+ * agent, never a colour.
17
+ */
18
+ agent?: string | null;
10
19
  className?: string;
11
20
  /** The scrolling body between the title bar and the footer. */
12
21
  bodyClassName?: string;
@@ -19,10 +28,17 @@ export interface AppWindowFrameProps {
19
28
  *
20
29
  * The frame is a column, so give it a height and the body fills what is left.
21
30
  */
22
- export function AppWindowFrame({ title, children, footer, className, bodyClassName }: AppWindowFrameProps) {
31
+ export function AppWindowFrame({ title, children, footer, agent, className, bodyClassName }: AppWindowFrameProps) {
32
+ const tint = useAgentTint(agent);
23
33
  return (
24
34
  <div
25
- className={cn('flex min-w-0 flex-col overflow-hidden rounded-xl border border-ods-border bg-ods-card', className)}
35
+ className={cn(
36
+ 'flex min-w-0 flex-col overflow-hidden rounded-xl border border-ods-border',
37
+ // An agent's window sits on the page surface under that agent's tint.
38
+ tint ? 'bg-ods-bg' : 'bg-ods-card',
39
+ className,
40
+ )}
41
+ style={tint}
26
42
  >
27
43
  <div className="relative flex h-10 shrink-0 items-center gap-2 border-b border-ods-border px-4" aria-hidden>
28
44
  <span className="h-2.5 w-2.5 rounded-full bg-ods-border" />
@@ -0,0 +1,97 @@
1
+ import type { ReactNode } from 'react';
2
+ import { cn } from '../../utils/cn';
3
+ import { trialLabels } from '../../utils/trial';
4
+ import { FileOffIcon } from '../icons-v2-generated/documents/file-off-icon';
5
+ import { CreditCardXmarkIcon } from '../icons-v2-generated/finance/credit-card-xmark-icon';
6
+ import { FlaskVialIcon } from '../icons-v2-generated/school/flask-vial-icon';
7
+ import { XmarkAltIcon } from '../icons-v2-generated/signs-and-symbols/xmark-alt-icon';
8
+
9
+ export interface BenefitItem {
10
+ /** Drawn at 16px in the quiet colour. */
11
+ icon?: ReactNode;
12
+ label: string;
13
+ }
14
+
15
+ /** The mark of each of the trial's terms, wherever a term is listed. */
16
+ export const TRIAL_TERM_ICONS = {
17
+ noCard: <CreditCardXmarkIcon />,
18
+ cancel: <XmarkAltIcon />,
19
+ length: <FlaskVialIcon />,
20
+ noContract: <FileOffIcon />,
21
+ } as const;
22
+ export type TrialTerm = keyof typeof TRIAL_TERM_ICONS;
23
+
24
+ /** The trial's terms a list shows when its host names none: cancel anytime, the trial's length, no contract. */
25
+ export const DEFAULT_TRIAL_TERMS: readonly TrialTerm[] = ['cancel', 'length', 'noContract'];
26
+
27
+ /** The named terms of the trial as benefits, in the order given. The length is left out when the host does not know it. */
28
+ export function trialBenefits(
29
+ trialDays: number | string | null | undefined,
30
+ terms: readonly TrialTerm[] = DEFAULT_TRIAL_TERMS,
31
+ ): BenefitItem[] {
32
+ const labels = trialLabels(trialDays);
33
+ return terms.flatMap(term => {
34
+ const label = labels[term];
35
+ return label ? [{ icon: TRIAL_TERM_ICONS[term], label }] : [];
36
+ });
37
+ }
38
+
39
+ export interface BenefitListProps {
40
+ /**
41
+ * Which of the trial's terms to list, in order ("cancel", "length",
42
+ * "noContract", "noCard"). Omitted: none, the list is `items` alone.
43
+ */
44
+ terms?: readonly TrialTerm[];
45
+ /** The free trial's length in days, for the `length` term. Without it that term is left out. */
46
+ trialDays?: number | string | null;
47
+ /** The host's own benefits. With `terms`, `itemsFirst` says which come first. */
48
+ items?: BenefitItem[];
49
+ /** Draw `items` before the trial's terms. Default: after them. */
50
+ itemsFirst?: boolean;
51
+ /** `center` centres the row (under a centred hero). Default `start`. */
52
+ align?: 'start' | 'center';
53
+ className?: string;
54
+ }
55
+
56
+ /**
57
+ * THE row of short benefits beside a call to action ("Cancel Anytime", "No
58
+ * Contract"): a small mark and a few words each, wrapping as a row. The one
59
+ * such row everywhere: the sign-up panel, every hero, every closing call to
60
+ * action. A host chooses WHICH benefits (`terms`, `items`), never how they look.
61
+ */
62
+ export function BenefitList({
63
+ terms,
64
+ trialDays,
65
+ items = [],
66
+ itemsFirst = false,
67
+ align = 'start',
68
+ className,
69
+ }: BenefitListProps) {
70
+ const termItems = terms ? trialBenefits(trialDays, terms) : [];
71
+ const all = itemsFirst ? [...items, ...termItems] : [...termItems, ...items];
72
+ if (all.length === 0) return null;
73
+ return (
74
+ <ul
75
+ className={cn(
76
+ // A full step between benefits, a tighter one between wrapped rows: each reads as its own point.
77
+ 'm-0 flex list-none flex-wrap content-center items-center gap-x-[var(--spacing-system-l)] gap-y-[var(--spacing-system-m)] p-0',
78
+ align === 'center' && 'justify-center',
79
+ className,
80
+ )}
81
+ >
82
+ {all.map(item => (
83
+ <li key={item.label} className="flex items-center gap-[var(--spacing-system-xs)]">
84
+ {item.icon && (
85
+ <span
86
+ className="flex size-4 shrink-0 items-center justify-center text-ods-text-secondary [&_svg]:size-4"
87
+ aria-hidden
88
+ >
89
+ {item.icon}
90
+ </span>
91
+ )}
92
+ <span className="whitespace-nowrap text-ods-text-primary text-h4">{item.label}</span>
93
+ </li>
94
+ ))}
95
+ </ul>
96
+ );
97
+ }
@@ -90,8 +90,19 @@ const buttonVariants = cva(
90
90
  true: 'px-0',
91
91
  false: '',
92
92
  },
93
+ // Two buttons drawn as ONE surface with a seam between them (a main action
94
+ // and a chevron that opens its alternatives: `DropdownButton` with an
95
+ // `action`). `start` is the leading half, `end` the trailing one: each
96
+ // gives up the corners and the border on its inner edge.
97
+ attach: {
98
+ none: '',
99
+ start: 'rounded-r-none',
100
+ end: 'rounded-l-none border-l-0',
101
+ },
93
102
  },
94
103
  compoundVariants: [
104
+ // The trailing half as an icon button is the design's 40px chevron cell, at every breakpoint.
105
+ { attach: 'end', size: 'icon', class: 'w-10 px-0 md:w-10' },
95
106
  { size: 'small', class: '[&_svg]:h-4 [&_svg]:w-4' },
96
107
  // Comes after the size classes, so cn()'s tailwind-merge resolves the
97
108
  // ods-typography group in favour of text-h4.
@@ -103,6 +114,7 @@ const buttonVariants = cva(
103
114
  font: 'bold',
104
115
  fullWidth: false,
105
116
  noPaddingX: false,
117
+ attach: 'none',
106
118
  },
107
119
  },
108
120
  );
@@ -267,6 +279,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function ButtonI
267
279
  font,
268
280
  fullWidth,
269
281
  noPaddingX,
282
+ attach,
270
283
  asChild,
271
284
  href,
272
285
  openInNewTab,
@@ -389,7 +402,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function ButtonI
389
402
  );
390
403
  }
391
404
 
392
- const classes = cn(buttonVariants({ variant, size, font, fullWidth, noPaddingX }), className);
405
+ const classes = cn(buttonVariants({ variant, size, font, fullWidth, noPaddingX, attach }), className);
393
406
 
394
407
  // asChild: consumer fully controls the rendered element; we just stamp our classes.
395
408
  if (asChild) {
@@ -6,9 +6,26 @@ import { Chevron02DownIcon } from '../icons-v2-generated';
6
6
  import { ActionsMenuDropdown, type ActionsMenuGroup, type ActionsMenuItem } from './actions-menu';
7
7
  import { Button, type ButtonProps } from './button';
8
8
 
9
+ /** The button's own action, when its label does something itself and the chevron holds the alternatives. */
10
+ export interface DropdownButtonAction {
11
+ href?: string;
12
+ /** Fetch `href` as a file (an installer) instead of navigating to it. */
13
+ download?: boolean | string;
14
+ openInNewTab?: boolean;
15
+ onClick?: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
16
+ }
17
+
9
18
  export interface DropdownButtonProps {
10
19
  /** Button label rendered next to the chevron. */
11
20
  label: React.ReactNode;
21
+ /**
22
+ * Makes the label a button of its own (Figma `button-action`): it runs this
23
+ * action, and the chevron beside it opens the menu of alternatives. Two click
24
+ * targets on one surface. Without it the whole button opens the menu.
25
+ */
26
+ action?: DropdownButtonAction;
27
+ /** The chevron's accessible name when `action` is set (it has no text of its own). Default: "More options". */
28
+ menuAriaLabel?: string;
12
29
  /** Optional leading icon rendered before the label. */
13
30
  icon?: React.ReactNode;
14
31
  /**
@@ -55,7 +72,9 @@ export interface DropdownButtonProps {
55
72
  * `ActionsMenu`. One component, so the trigger styling, chevron, and menu
56
73
  * chrome cannot drift between surfaces.
57
74
  *
58
- * Two trigger looks — BOTH render through the unified `Button`:
75
+ * With `action`, the label is a button of its own and the chevron alone opens
76
+ * the menu (two targets, one surface). Otherwise, two trigger looks — BOTH
77
+ * render through the unified `Button`:
59
78
  * - default (no `variant`): the card-colored seam trigger — `Button
60
79
  * variant="outline"` with the chevron in its `splitIcon` slot, so the label
61
80
  * and chevron are separated by a divider, the whole surface is one click
@@ -70,6 +89,8 @@ export interface DropdownButtonProps {
70
89
  */
71
90
  export function DropdownButton({
72
91
  label,
92
+ action,
93
+ menuAriaLabel = 'More options',
73
94
  icon,
74
95
  items,
75
96
  groups,
@@ -107,6 +128,57 @@ export function DropdownButton({
107
128
 
108
129
  const resolvedAriaLabel = ariaLabel || (typeof label === 'string' ? label : undefined);
109
130
 
131
+ // With its own action: two common `Button`s attached into one surface. The
132
+ // label runs the action, the chevron is the menu's trigger.
133
+ if (action) {
134
+ const skin = variant ?? 'outline';
135
+ return (
136
+ <div
137
+ role="group"
138
+ aria-label={resolvedAriaLabel}
139
+ className={cn('inline-flex items-stretch', fullWidth && 'w-full', className)}
140
+ >
141
+ <Button
142
+ variant={skin}
143
+ attach="start"
144
+ href={action.href}
145
+ download={action.download}
146
+ openInNewTab={action.openInNewTab}
147
+ onClick={action.onClick as React.MouseEventHandler<HTMLButtonElement> | undefined}
148
+ disabled={disabled}
149
+ loading={loading}
150
+ leftIcon={icon}
151
+ className={fullWidth ? 'flex-1' : undefined}
152
+ >
153
+ {label}
154
+ </Button>
155
+ <ActionsMenuDropdown
156
+ groups={groups ?? [{ items: items ?? [] }]}
157
+ open={open}
158
+ onOpenChange={handleOpenChange}
159
+ align={align}
160
+ side={side}
161
+ onCloseAutoFocus={onCloseAutoFocus}
162
+ triggerAriaLabel={menuAriaLabel}
163
+ customTrigger={
164
+ <Button
165
+ type="button"
166
+ variant={skin}
167
+ size="icon"
168
+ attach="end"
169
+ disabled={disabled || loading}
170
+ aria-label={menuAriaLabel}
171
+ className={cn(open && 'bg-ods-bg-hover')}
172
+ leftIcon={
173
+ <Chevron02DownIcon className={cn('transition-transform duration-fast', open && 'rotate-180')} />
174
+ }
175
+ />
176
+ }
177
+ />
178
+ </div>
179
+ );
180
+ }
181
+
110
182
  const trigger = variant ? (
111
183
  <Button
112
184
  variant={variant}
@@ -240,6 +240,9 @@ export * from './product-screen-frame';
240
240
  export * from './phone-frame';
241
241
  export * from './notification-stack';
242
242
  export * from './numbered-step';
243
+ export * from './strip-card';
244
+ export * from './benefit-list';
245
+ export * from './leader-row';
243
246
  export * from './device-wallpaper';
244
247
  export * from './analyzing-state';
245
248
  export * from './incident-feed';
@@ -0,0 +1,28 @@
1
+ import type { ReactNode } from 'react';
2
+ import { cn } from '../../utils/cn';
3
+
4
+ export interface LeaderRowProps {
5
+ label: ReactNode;
6
+ value: ReactNode;
7
+ /** After the value, at 16px (a check, a warning). */
8
+ status?: ReactNode;
9
+ /** The row's element. Default `div`; `li` inside a list. */
10
+ as?: 'div' | 'li';
11
+ className?: string;
12
+ }
13
+
14
+ /**
15
+ * A label and its value joined by a leader line (Figma `info-card` row): the
16
+ * quiet label, a hairline that takes the free space, the value, and a status
17
+ * mark at the end. One line, always: a long value clips.
18
+ */
19
+ export function LeaderRow({ label, value, status, as: Row = 'div', className }: LeaderRowProps) {
20
+ return (
21
+ <Row className={cn('flex min-w-0 items-center gap-[var(--spacing-system-xsf)] text-h4', className)}>
22
+ <span className="shrink-0 whitespace-nowrap text-ods-text-secondary">{label}</span>
23
+ <span className="h-px min-w-4 flex-1 bg-ods-border" aria-hidden />
24
+ <span className="min-w-0 truncate text-ods-text-primary">{value}</span>
25
+ {status && <span className="flex shrink-0 items-center">{status}</span>}
26
+ </Row>
27
+ );
28
+ }
@@ -0,0 +1,68 @@
1
+ import type { ReactNode } from 'react';
2
+ import { cn } from '../../utils/cn';
3
+
4
+ export interface StripCardProps {
5
+ /** Before the title, at 24px (an icon, a step's number). */
6
+ lead?: ReactNode;
7
+ title: ReactNode;
8
+ /** One quieter line under the title. */
9
+ caption?: ReactNode;
10
+ /** The title's element. Default `h3`. */
11
+ titleAs?: 'h3' | 'h4' | 'p';
12
+ /** `roomy` gives the head and the body the larger inset (a card whose head carries a caption). */
13
+ density?: 'default' | 'roomy';
14
+ children?: ReactNode;
15
+ className?: string;
16
+ bodyClassName?: string;
17
+ }
18
+
19
+ /**
20
+ * THE card with a head strip (Figma "Info-card" with a `tag` head): a head on
21
+ * the card surface holding a mark, a title and an optional caption, a hairline,
22
+ * then the body on the page surface. One frame for every such card, so a row
23
+ * of them shares its radius, hairlines and insets.
24
+ */
25
+ export function StripCard({
26
+ lead,
27
+ title,
28
+ caption,
29
+ titleAs: Title = 'h3',
30
+ density = 'default',
31
+ children,
32
+ className,
33
+ bodyClassName,
34
+ }: StripCardProps) {
35
+ const roomy = density === 'roomy';
36
+ return (
37
+ <div
38
+ className={cn(
39
+ 'flex h-full min-w-0 flex-col overflow-hidden rounded-md border border-ods-border bg-ods-card',
40
+ className,
41
+ )}
42
+ >
43
+ <div
44
+ className={cn(
45
+ 'flex items-center gap-[var(--spacing-system-mf)] border-b border-ods-border',
46
+ roomy ? 'p-[var(--spacing-system-l)]' : 'p-[var(--spacing-system-m)]',
47
+ )}
48
+ >
49
+ {lead && <span className="flex shrink-0 items-center">{lead}</span>}
50
+ <div className="flex min-w-0 flex-1 flex-col">
51
+ <Title className="m-0 text-ods-text-primary text-h3">{title}</Title>
52
+ {caption && <span className="text-ods-text-secondary text-h6">{caption}</span>}
53
+ </div>
54
+ </div>
55
+ <div
56
+ className={cn(
57
+ 'flex min-w-0 flex-1 flex-col bg-ods-bg',
58
+ roomy
59
+ ? 'gap-[var(--spacing-system-l)] p-[var(--spacing-system-l)]'
60
+ : 'gap-[var(--spacing-system-m)] px-[var(--spacing-system-m)] pb-[var(--spacing-system-l)] pt-[var(--spacing-system-m)]',
61
+ bodyClassName,
62
+ )}
63
+ >
64
+ {children}
65
+ </div>
66
+ </div>
67
+ );
68
+ }
@@ -56,11 +56,11 @@ export interface AssistantRuntime {
56
56
  source?: string;
57
57
  /**
58
58
  * GET endpoint the questions are read from:
59
- * `<url>?count=<n>&section=<topic>&exclude=<id,id>` → `{ prompts: AskPrompt[] }`
60
- * `section` is ANY string: the host decides what topics exist (the hub: the
61
- * `section` attribute of a quick action, set in chat config), so a new topic
62
- * needs no change here. Hub: '/api/quick-actions/questions'. Absent: an "ask"
63
- * surface shows its launcher with no questions.
59
+ * `<url>?count=<n>&topic=<topic>&exclude=<id,id>` → `{ prompts: AskPrompt[] }`
60
+ * `topic` is ANY string: the host decides what topics exist (the hub: the
61
+ * topics of its FAQs, set in the FAQ admin), so a new topic needs no change
62
+ * here. Hub: '/api/faqs/questions'. Absent: an "ask" surface shows its
63
+ * launcher with no questions.
64
64
  */
65
65
  askPromptsUrl?: string;
66
66
  /**
package/src/types/faq.ts CHANGED
@@ -1,8 +1,26 @@
1
+ /**
2
+ * The two kinds of FAQ. An `answer` is a question with its written answer (the
3
+ * accordion, FAQPage JSON-LD). An `ask` is a question the assistant answers: it
4
+ * has a `prompt` (what is sent to the chat) and no written answer, and is what
5
+ * every "ask the assistant" surface offers.
6
+ */
7
+ export const FAQ_KINDS = ['answer', 'ask'] as const;
8
+ export type FaqKind = (typeof FAQ_KINDS)[number];
9
+
1
10
  export interface Faq {
2
11
  id: number;
3
12
  question: string;
4
13
  answer: string;
5
14
  section?: string | null;
15
+ /** Absent on a host that predates kinds: an `answer`. */
16
+ kind?: FaqKind;
17
+ /** An `ask` FAQ's prompt for the chat; null on an `answer`. */
18
+ prompt?: string | null;
19
+ /**
20
+ * The places the FAQ is written for, shared by both kinds: a page's topic
21
+ * (`pricing`, `careers`, `home`) or a section's. Empty: written for no place.
22
+ */
23
+ topics?: string[];
6
24
  display_order: number;
7
25
  is_active: boolean;
8
26
  created_at: string;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Times for a scripted conversation (a product demo, an example card): a
3
+ * believable time of day for each turn, chosen from a seed.
4
+ *
5
+ * The same seed always gives the same times, so a server render and the
6
+ * browser agree and a card keeps its time on every visit. Turns of one
7
+ * conversation follow each other: each is the one before it plus a short gap.
8
+ */
9
+
10
+ /** A 32-bit hash of the seed (FNV-1a), the generator's starting state. */
11
+ function hashSeed(seed: string): number {
12
+ let hash = 0x811c9dc5;
13
+ for (let i = 0; i < seed.length; i++) {
14
+ hash ^= seed.charCodeAt(i);
15
+ hash = Math.imul(hash, 0x01000193);
16
+ }
17
+ return hash >>> 0;
18
+ }
19
+
20
+ /** A seeded generator of numbers in [0, 1) (mulberry32). */
21
+ function seededRandom(seed: string): () => number {
22
+ let state = hashSeed(seed);
23
+ return () => {
24
+ state = (state + 0x6d2b79f5) >>> 0;
25
+ let t = state;
26
+ t = Math.imul(t ^ (t >>> 15), t | 1);
27
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
28
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
29
+ };
30
+ }
31
+
32
+ export interface DemoClockOptions {
33
+ /** How many turns to time. Default 2. */
34
+ count?: number;
35
+ /** The first turn falls between these hours of the day (24h, end exclusive). Default 8 to 18. */
36
+ fromHour?: number;
37
+ toHour?: number;
38
+ /** The longest gap between two turns, in minutes. Default 3. A gap may be 0: an agent answers within the minute. */
39
+ maxGapMinutes?: number;
40
+ }
41
+
42
+ /** Minutes of the day as "2:47 PM". */
43
+ export function formatDemoClock(minutesOfDay: number): string {
44
+ const minutes = ((Math.floor(minutesOfDay) % 1440) + 1440) % 1440;
45
+ const hour24 = Math.floor(minutes / 60);
46
+ const hour12 = hour24 % 12 === 0 ? 12 : hour24 % 12;
47
+ return `${hour12}:${String(minutes % 60).padStart(2, '0')} ${hour24 < 12 ? 'AM' : 'PM'}`;
48
+ }
49
+
50
+ /** The time of each turn of one scripted conversation, in order, for `seed`. */
51
+ export function demoClockTimes(seed: string, options: DemoClockOptions = {}): string[] {
52
+ const { count = 2, fromHour = 8, toHour = 18, maxGapMinutes = 3 } = options;
53
+ const random = seededRandom(seed);
54
+ const span = Math.max(1, (toHour - fromHour) * 60);
55
+ let minutes = fromHour * 60 + Math.floor(random() * span);
56
+ const times: string[] = [];
57
+ for (let i = 0; i < Math.max(0, count); i++) {
58
+ if (i > 0) minutes += Math.floor(random() * (maxGapMinutes + 1));
59
+ times.push(formatDemoClock(minutes));
60
+ }
61
+ return times;
62
+ }
@@ -0,0 +1,23 @@
1
+ import type { CSSProperties } from 'react';
2
+
3
+ /**
4
+ * A surface tinted by an identity colour (an agent's): its border mixed from
5
+ * the colour, a faint wash of it over the page surface and a glow of it in the
6
+ * far corner. THE tint of every card and window that belongs to one agent, so
7
+ * they read as that agent's at a glance and all read alike.
8
+ *
9
+ * `accent` is a CSS colour (the hex an agent's identity stores). The surface
10
+ * under it should be the page surface (`bg-ods-bg`). Undefined gives no style,
11
+ * so a surface with no agent keeps its own border and background.
12
+ */
13
+ export function identityTint(accent: string | null | undefined): CSSProperties | undefined {
14
+ if (!accent) return undefined;
15
+ const wash = `color-mix(in srgb, ${accent} 2%, transparent)`;
16
+ return {
17
+ borderColor: `color-mix(in srgb, ${accent} 30%, transparent)`,
18
+ backgroundImage: [
19
+ `radial-gradient(120% 120% at 100% 100%, color-mix(in srgb, ${accent} 4%, transparent), transparent 70%)`,
20
+ `linear-gradient(${wash}, ${wash})`,
21
+ ].join(', '),
22
+ };
23
+ }
@@ -473,3 +473,5 @@ export type { ProgramDateFields, ProgramInstant, ProgramMetaRenderers } from './
473
473
  export { copyrightLine } from './copyright-line';
474
474
  export * from './path-pattern';
475
475
  export * from './random-id';
476
+ export * from './demo-clock';
477
+ export * from './identity-tint';