@hraness/design-kit 0.34.0 → 0.35.1

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 (50) hide show
  1. package/AGENT_SETUP.md +4 -0
  2. package/ARTICLE_COPY.md +12 -6
  3. package/README.md +20 -2
  4. package/dist/{chunk-nwpcjm48.js → chunk-6bfr0g23.js} +1115 -441
  5. package/dist/{chunk-h4k7yv6x.js → chunk-6ts4955n.js} +36 -36
  6. package/dist/chunk-gmea6p0d.js +13 -0
  7. package/dist/{chunk-mr1vdcjq.js → chunk-tzd7177a.js} +142 -82
  8. package/dist/index.js +5 -1
  9. package/dist/mockups/client.js +50 -15
  10. package/dist/portfolio.js +586 -85
  11. package/dist/react/index.js +169 -105
  12. package/dist/react/platform-install.js +3 -2
  13. package/dist/react/server.js +17 -3
  14. package/dist/stylex-manifest.json +1 -1
  15. package/dist/stylex.css +376 -58
  16. package/package.json +16 -3
  17. package/portfolio-inventory.json +1 -1
  18. package/src/diagrams.ts +11 -0
  19. package/src/index.ts +2 -0
  20. package/src/marketing-comparison.css +162 -0
  21. package/src/marketing-diagram.css +24 -0
  22. package/src/mockups/client.tsx +25 -17
  23. package/src/mockups.css +19 -0
  24. package/src/plain-publication.css +9 -6
  25. package/src/portfolio.generated.json +578 -86
  26. package/src/portfolio.generated.ts +578 -86
  27. package/src/portfolio.ts +73 -0
  28. package/src/product-marketing-foundation.css +29 -0
  29. package/src/product-marketing.css +133 -38
  30. package/src/react/agent-setup-prompt.stylex.ts +5 -25
  31. package/src/react/agent-setup-prompt.tsx +9 -18
  32. package/src/react/article.tsx +2 -2
  33. package/src/react/design-gallery.tsx +34 -6
  34. package/src/react/index.ts +3 -0
  35. package/src/react/marketing-account.stylex.ts +74 -0
  36. package/src/react/marketing-account.tsx +69 -0
  37. package/src/react/marketing-comparison.stylex.ts +24 -0
  38. package/src/react/marketing-comparison.tsx +73 -0
  39. package/src/react/marketing-diagram.stylex.ts +8 -0
  40. package/src/react/marketing-diagram.tsx +33 -0
  41. package/src/react/platform-install.stylex.ts +9 -37
  42. package/src/react/platform-install.tsx +17 -24
  43. package/src/react/product-marketing.stylex.ts +138 -109
  44. package/src/react/product-marketing.tsx +69 -43
  45. package/src/react/server.ts +3 -0
  46. package/src/react/setup-copy-button.stylex.ts +39 -0
  47. package/src/react/setup-copy-button.tsx +20 -0
  48. package/src/styles.css +2 -0
  49. package/src/typography.css +32 -0
  50. /package/dist/{chunk-4d79eskk.js → chunk-c80g58j1.js} +0 -0
package/src/portfolio.ts CHANGED
@@ -108,11 +108,39 @@ export type PortfolioProvenance = Readonly<{
108
108
  files: readonly Readonly<{ path: string; sha256: string }>[];
109
109
  }>;
110
110
 
111
+ export type PortfolioPackTone = "rose" | "indigo" | "amber" | "emerald";
112
+
113
+ /** The homepage categories, in the same order as hraness.com. */
114
+ export type PortfolioPack = Readonly<{
115
+ slug: string;
116
+ title: string;
117
+ tone: PortfolioPackTone;
118
+ members: readonly PortfolioProductId[];
119
+ }>;
120
+
121
+ export type PortfolioStudioItem = Readonly<{
122
+ productId: PortfolioProductId;
123
+ href: string;
124
+ name: string;
125
+ role: string;
126
+ mark: string;
127
+ domain: string;
128
+ }>;
129
+
130
+ /** Product-neutral data shaped for the shared MarketingRelated composition. */
131
+ export type PortfolioStudioGroup = Readonly<{
132
+ heading: string;
133
+ headingId: string;
134
+ tone: PortfolioPackTone | "neutral";
135
+ items: readonly PortfolioStudioItem[];
136
+ }>;
137
+
111
138
  export type PortfolioSnapshot = Readonly<{
112
139
  contract: "hraness.design-kit-portfolio/v1";
113
140
  formatVersion: 1;
114
141
  provenance: PortfolioProvenance;
115
142
  products: Readonly<Record<PortfolioProductId, PortfolioProduct>>;
143
+ packs: readonly PortfolioPack[];
116
144
  relations: readonly PortfolioRelation[];
117
145
  /** SHA-256 of the snapshot's compact JSON without this key. Pin it in a consumer test. */
118
146
  digest: string;
@@ -154,6 +182,7 @@ function deepFreeze<T>(value: T): T {
154
182
  /** The whole snapshot, frozen so one consumer cannot change another's facts at runtime. */
155
183
  export const portfolioFacts: PortfolioSnapshot = deepFreeze(portfolioSnapshot);
156
184
  export const portfolioProducts: PortfolioSnapshot["products"] = portfolioFacts.products;
185
+ export const portfolioPacks: readonly PortfolioPack[] = portfolioFacts.packs;
157
186
  export const portfolioRelations: PortfolioSnapshot["relations"] = portfolioFacts.relations;
158
187
  export const portfolioProvenance: PortfolioProvenance = portfolioFacts.provenance;
159
188
  export const portfolioDigest: string = portfolioFacts.digest;
@@ -224,3 +253,47 @@ export function usesPairs(): readonly PortfolioUsesPair[] {
224
253
  target: product(relation.target),
225
254
  }));
226
255
  }
256
+
257
+ /**
258
+ * Present only the caller's selected products, once each, in the homepage's
259
+ * category and member order. Categories with no selected products disappear.
260
+ * Products outside a pack follow in a neutral group. This does not infer an
261
+ * integration or expand a site's curated related-product selection.
262
+ */
263
+ export function portfolioRelatedGroups(
264
+ ids: readonly PortfolioProductId[],
265
+ headingPrefix = "related",
266
+ ): readonly PortfolioStudioGroup[] {
267
+ if (!/^[a-z][a-z0-9-]*$/u.test(headingPrefix)) {
268
+ throw new PortfolioFactsError("The related heading prefix must be a lowercase HTML identifier.");
269
+ }
270
+ const selected = new Set(ids.map((id) => product(id).id));
271
+ const item = (id: PortfolioProductId): PortfolioStudioItem => {
272
+ const entry = product(id);
273
+ return {
274
+ productId: id,
275
+ href: entry.canonicalUrl,
276
+ name: entry.messaging.names.name,
277
+ role: entry.oneLiner,
278
+ mark: entry.mark,
279
+ domain: entry.canonicalUrl.replace(/^https?:\/\/(?:www\.)?([^/:?#]+).*$/u, "$1"),
280
+ };
281
+ };
282
+ const groups: PortfolioStudioGroup[] = [];
283
+ for (const pack of portfolioPacks) {
284
+ const members = pack.members.filter((id) => selected.delete(id));
285
+ if (members.length > 0) groups.push({
286
+ heading: pack.title,
287
+ headingId: `${headingPrefix}-${pack.slug}`,
288
+ tone: pack.tone,
289
+ items: members.map(item),
290
+ });
291
+ }
292
+ if (selected.size > 0) groups.push({
293
+ heading: "More from Hraness",
294
+ headingId: `${headingPrefix}-more`,
295
+ tone: "neutral",
296
+ items: portfolioProductIds.filter((id) => selected.has(id)).map(item),
297
+ });
298
+ return groups;
299
+ }
@@ -108,6 +108,21 @@
108
108
  text-underline-offset: 0.16em;
109
109
  }
110
110
 
111
+
112
+ .hraness-marketing-page :where(p, li, dd, dt, figcaption) a:not([role="button"], [data-emphasis], [data-hraness-marketing], [data-foil]),
113
+ .hraness-marketing-maker__links a {
114
+ text-decoration-line: underline;
115
+ text-decoration-style: dotted;
116
+ text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
117
+ text-decoration-thickness: 1px;
118
+ text-underline-offset: 0.2em;
119
+ }
120
+
121
+ .hraness-marketing-page :where(p, li, dd, dt, figcaption) a:not([role="button"], [data-emphasis], [data-hraness-marketing], [data-foil]):is(:hover, :focus-visible),
122
+ .hraness-marketing-maker__links a:is(:hover, :focus-visible) {
123
+ text-decoration-color: currentColor;
124
+ }
125
+
111
126
  .hraness-marketing-page :where(code, kbd, pre) {
112
127
  font-family: var(--hraness-marketing-mono-font);
113
128
  }
@@ -431,3 +446,17 @@ html:has(:is(.hraness-marketing-header, .hraness-marketing-header-surface):not([
431
446
  }
432
447
 
433
448
  }
449
+
450
+ /* Product-owned SVG content grammar; the diagram's React frame stays compiled. */
451
+ @layer components.hraness-design-kit.legacy {
452
+ .hraness-diagram .hraness-diagram__label { fill: var(--foreground); font-family: var(--font-text); font-size: 20px; font-weight: 500; }
453
+ .hraness-diagram .hraness-diagram__detail { fill: var(--muted); font-family: var(--font-text); font-size: 16px; font-weight: 400; }
454
+ .hraness-diagram .hraness-diagram__node { fill: var(--background); stroke: var(--line); stroke-width: 1; }
455
+ .hraness-diagram .hraness-diagram__connector,
456
+ .hraness-diagram .hraness-diagram__icon { color: var(--primary); fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
457
+ @media (forced-colors: active) {
458
+ .hraness-diagram .hraness-diagram__label, .hraness-diagram .hraness-diagram__detail { fill: CanvasText; }
459
+ .hraness-diagram .hraness-diagram__node { fill: Canvas; stroke: CanvasText; }
460
+ .hraness-diagram .hraness-diagram__connector, .hraness-diagram .hraness-diagram__icon { color: CanvasText; }
461
+ }
462
+ }
@@ -101,6 +101,21 @@
101
101
  text-underline-offset: 0.16em;
102
102
  }
103
103
 
104
+
105
+ .hraness-marketing-page :where(p, li, dd, dt, figcaption) a:not([role="button"], [data-emphasis], [data-hraness-marketing], [data-foil]),
106
+ .hraness-marketing-maker__links a {
107
+ text-decoration-line: underline;
108
+ text-decoration-style: dotted;
109
+ text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
110
+ text-decoration-thickness: 1px;
111
+ text-underline-offset: 0.2em;
112
+ }
113
+
114
+ .hraness-marketing-page :where(p, li, dd, dt, figcaption) a:not([role="button"], [data-emphasis], [data-hraness-marketing], [data-foil]):is(:hover, :focus-visible),
115
+ .hraness-marketing-maker__links a:is(:hover, :focus-visible) {
116
+ text-decoration-color: currentColor;
117
+ }
118
+
104
119
  .hraness-marketing-page :where(code, kbd, pre) {
105
120
  font-family: var(--hraness-marketing-mono-font);
106
121
  }
@@ -749,6 +764,8 @@ html:has(:is(.hraness-marketing-header, .hraness-marketing-header-surface):not([
749
764
  line-height: var(--hraness-marketing-summary-leading, 1.6);
750
765
  }
751
766
 
767
+ .hraness-marketing-hero__install { inline-size: min(100%, 44rem); min-inline-size: 0; margin-block-start: 0.75rem; text-align: start; }
768
+
752
769
  .hraness-marketing-hero__example {
753
770
  max-inline-size: var(--hraness-marketing-example-measure, var(--hraness-marketing-copy-measure));
754
771
  margin-block-start: -0.35rem;
@@ -1029,6 +1046,11 @@ html:has(:is(.hraness-marketing-header, .hraness-marketing-header-surface):not([
1029
1046
  line-height: 1.5;
1030
1047
  }
1031
1048
 
1049
+ .hraness-marketing-pillars[data-presentation="benefits"] { border-block: 0; gap: 2.75rem 3rem; }
1050
+ .hraness-marketing-pillars[data-presentation="benefits"] > div { border: 0; padding: 0; gap: 0.75rem; }
1051
+ .hraness-marketing-pillars__icon { display: block; inline-size: 1.5rem; block-size: 1.5rem; margin-block-end: 1.1rem; color: var(--primary); }
1052
+ .hraness-marketing-pillars__icon > svg { inline-size: 100%; block-size: 100%; }
1053
+
1032
1054
  /* Product frame */
1033
1055
 
1034
1056
  .hraness-marketing-proof-frame {
@@ -1345,12 +1367,28 @@ html:has(:is(.hraness-marketing-header, .hraness-marketing-header-surface):not([
1345
1367
  max-inline-size: none;
1346
1368
  }
1347
1369
 
1348
- /* Related: labeled tiers of sibling cards */
1370
+ /* Studio products use the portfolio category rule and compact mark rows. */
1371
+
1372
+ .hraness-marketing-related__groups {
1373
+ display: grid;
1374
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr));
1375
+ gap: 2.5rem 3.5rem;
1376
+ align-items: start;
1377
+ }
1349
1378
 
1350
1379
  .hraness-marketing-related__group {
1380
+ --hraness-marketing-related-tone: var(--hraness-marketing-ink);
1351
1381
  display: grid;
1352
- gap: 1.5rem;
1382
+ gap: 1rem;
1353
1383
  min-inline-size: 0;
1384
+ border-block-start: 2px solid var(--hraness-marketing-related-tone);
1385
+ padding-block-start: 1rem;
1386
+ }
1387
+
1388
+ @media (forced-colors: active) {
1389
+ .hraness-marketing-related__group {
1390
+ --hraness-marketing-related-tone: CanvasText;
1391
+ }
1354
1392
  }
1355
1393
 
1356
1394
  .hraness-marketing-related__group-header {
@@ -1359,93 +1397,151 @@ html:has(:is(.hraness-marketing-header, .hraness-marketing-header-surface):not([
1359
1397
  gap: 0.5rem;
1360
1398
  }
1361
1399
 
1362
- /* Tier headings take the third heading size so a display face never renders
1363
- * at caption size. */
1364
1400
  .hraness-marketing-related__group-heading {
1365
1401
  margin: 0;
1366
1402
  color: var(--hraness-marketing-ink);
1367
- font-family: var(--hraness-marketing-heading-font);
1368
- font-weight: var(--hraness-marketing-heading-weight);
1369
- letter-spacing: var(--hraness-marketing-h2-tracking, -0.012em);
1370
- line-height: var(--hraness-marketing-h3-leading, 1.15);
1403
+ font-family: var(--hraness-marketing-text-font);
1404
+ font-weight: 600;
1405
+ letter-spacing: -0.01em;
1406
+ line-height: 1.35;
1371
1407
  text-wrap: balance;
1372
- font-size: var(--hraness-marketing-h3-size, clamp(1.75rem, 1.35rem + 1.6vw, 2.5rem));
1408
+ font-size: 1.125rem;
1373
1409
  }
1374
1410
 
1375
1411
  .hraness-marketing-related__group-summary {
1376
1412
  margin: 0;
1377
1413
  color: var(--hraness-marketing-muted);
1378
1414
  font-family: var(--hraness-marketing-text-font);
1379
- font-size: 1.125rem;
1415
+ font-size: 1rem;
1380
1416
  line-height: 1.5;
1381
1417
  }
1382
1418
 
1383
- /* A related product is one linked row, as on the hraness.com project index:
1384
- * its mark, its name, and its one-line description. Names and descriptions
1385
- * stay in the text face at reading sizes. */
1419
+ .hraness-marketing-related__list {
1420
+ --hraness-marketing-grid-columns: initial;
1421
+ --_hraness-marketing-grid-track: max(16rem, calc((100% - (var(--hraness-marketing-grid-columns) - 1) * 0.5rem) / var(--hraness-marketing-grid-columns)));
1422
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--_hraness-marketing-grid-track, 100%)), 1fr));
1423
+ display: grid;
1424
+ gap: 0.5rem;
1425
+ list-style: none;
1426
+ margin: 0;
1427
+ padding: 0;
1428
+ min-inline-size: 0;
1429
+ }
1430
+
1431
+ .hraness-marketing-related__item {
1432
+ margin: 0;
1433
+ min-inline-size: 0;
1434
+ }
1435
+
1386
1436
  .hraness-marketing-related__card {
1387
1437
  display: flex;
1388
1438
  flex-direction: row;
1389
- align-items: center;
1439
+ align-items: flex-start;
1390
1440
  position: relative;
1391
- isolation: isolate;
1392
1441
  min-inline-size: 0;
1393
- min-block-size: 100%;
1394
- align-self: stretch;
1395
- gap: 1rem;
1396
- padding: clamp(1rem, 2vw, 1.25rem) clamp(1.125rem, 2.5vw, 1.5rem);
1397
- border: var(--hraness-marketing-surface-rule);
1398
- border-radius: var(--hraness-marketing-frame-radius);
1399
- background: var(--hraness-marketing-surface);
1400
- color: inherit;
1442
+ min-block-size: 2.75rem;
1443
+ gap: 0.85rem;
1444
+ padding: 0.55rem 0.5rem;
1445
+ margin-inline: 0;
1446
+ border: 0;
1447
+ border-radius: 0.375rem;
1448
+ background-color: transparent;
1449
+ color: var(--hraness-marketing-ink);
1401
1450
  text-decoration: none;
1402
- box-shadow: var(--hraness-marketing-surface-shadow);
1451
+ box-shadow: none;
1403
1452
  }
1404
1453
 
1405
1454
  .hraness-marketing-related__card:hover {
1406
- box-shadow: 0 0 0 1px var(--hraness-marketing-line-strong);
1455
+ background-color: color-mix(in srgb, var(--hraness-marketing-related-tone, var(--hraness-marketing-ink)) 6%, transparent);
1407
1456
  }
1408
1457
 
1409
1458
  .hraness-marketing-related__card:focus-visible {
1410
- outline: 2px solid var(--hraness-marketing-accent);
1411
- outline-offset: 2px;
1459
+ outline: 2px solid var(--hraness-marketing-ink);
1460
+ outline-offset: -2px;
1412
1461
  }
1413
1462
 
1414
1463
  .hraness-marketing-related__card-mark {
1464
+ --hraness-foil-text-base: var(--hraness-marketing-related-tone, var(--hraness-marketing-ink));
1465
+ color: var(--hraness-marketing-related-tone, var(--hraness-marketing-ink));
1466
+ flex-grow: 0;
1467
+ flex-shrink: 0;
1468
+ flex-basis: auto;
1415
1469
  display: inline-flex;
1416
- flex: 0 0 auto;
1417
1470
  align-items: center;
1418
1471
  justify-content: center;
1419
- inline-size: 2.75rem;
1420
- block-size: 2.75rem;
1472
+ inline-size: 1.75rem;
1473
+ block-size: 1.75rem;
1474
+ margin-block-start: 0.1rem;
1421
1475
  }
1422
1476
 
1423
1477
  .hraness-marketing-related__card-text {
1424
1478
  display: grid;
1425
- gap: 0.25rem;
1479
+ gap: 0.05rem;
1426
1480
  min-inline-size: 0;
1427
1481
  }
1428
1482
 
1483
+ .hraness-marketing-related__card-heading {
1484
+ display: flex;
1485
+ flex-wrap: wrap;
1486
+ align-items: baseline;
1487
+ column-gap: 0.6rem;
1488
+ }
1489
+
1429
1490
  .hraness-marketing-related__card-name {
1430
1491
  margin: 0;
1431
1492
  color: var(--hraness-marketing-ink);
1432
1493
  font-family: var(--hraness-marketing-text-font);
1433
- font-size: 1.25rem;
1434
- font-weight: 700;
1494
+ font-size: 1.0625rem;
1495
+ font-weight: 600;
1435
1496
  letter-spacing: -0.01em;
1436
- line-height: 1.25;
1497
+ line-height: 1.35;
1498
+ overflow-wrap: anywhere;
1499
+ text-decoration-line: underline;
1500
+ text-decoration-style: dotted;
1501
+ text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
1502
+ text-decoration-thickness: 1px;
1503
+ text-underline-offset: 0.2em;
1504
+ }
1505
+
1506
+ .hraness-marketing-related__card-domain {
1507
+ color: var(--hraness-marketing-muted);
1508
+ font-family: var(--hraness-marketing-text-font);
1509
+ font-size: 0.8125rem;
1510
+ font-weight: 400;
1511
+ line-height: 1.5;
1437
1512
  overflow-wrap: anywhere;
1438
1513
  }
1439
1514
 
1440
1515
  .hraness-marketing-related__card-role {
1441
1516
  margin: 0;
1442
- color: var(--hraness-marketing-ink);
1517
+ color: var(--hraness-marketing-muted);
1443
1518
  font-family: var(--hraness-marketing-text-font);
1444
- font-size: 1.0625rem;
1519
+ font-size: 0.9375rem;
1445
1520
  line-height: 1.45;
1521
+ overflow-wrap: anywhere;
1446
1522
  text-wrap: pretty;
1447
1523
  }
1448
1524
 
1525
+ .hraness-marketing-related__group[data-tone="rose"] {
1526
+ --hraness-marketing-related-tone: var(--hraness-studio-rose);
1527
+ }
1528
+
1529
+ .hraness-marketing-related__group[data-tone="indigo"] {
1530
+ --hraness-marketing-related-tone: var(--hraness-studio-indigo);
1531
+ }
1532
+
1533
+ .hraness-marketing-related__group[data-tone="amber"] {
1534
+ --hraness-marketing-related-tone: var(--hraness-studio-amber);
1535
+ }
1536
+
1537
+ .hraness-marketing-related__group[data-tone="emerald"] {
1538
+ --hraness-marketing-related-tone: var(--hraness-studio-emerald);
1539
+ }
1540
+
1541
+ @media (forced-colors: active) {
1542
+ .hraness-marketing-related__group[data-tone] { --hraness-marketing-related-tone: CanvasText; }
1543
+ }
1544
+
1449
1545
  /* Primitives: numbered building blocks */
1450
1546
 
1451
1547
  .hraness-marketing-primitives {
@@ -1720,7 +1816,7 @@ html:has(:is(.hraness-marketing-header, .hraness-marketing-header-surface):not([
1720
1816
  }
1721
1817
 
1722
1818
  .hraness-marketing-quote figcaption a:hover {
1723
- text-decoration: underline;
1819
+ text-decoration-line: underline;
1724
1820
  }
1725
1821
 
1726
1822
  /* Pricing */
@@ -2126,7 +2222,6 @@ html:has(:is(.hraness-marketing-header, .hraness-marketing-header-surface):not([
2126
2222
  .hraness-marketing-trust-item,
2127
2223
  .hraness-marketing-card,
2128
2224
  .hraness-marketing-card__art,
2129
- .hraness-marketing-related__card,
2130
2225
  .hraness-marketing-quote,
2131
2226
  .hraness-marketing-plan {
2132
2227
  border: 1px solid CanvasText;
@@ -89,39 +89,19 @@ export const agentSetupStyles = stylex.create({
89
89
  ":hover": { color: ink },
90
90
  "::marker": { content: '""' },
91
91
  },
92
- copy: {
93
- alignItems: "center",
94
- appearance: "none",
95
- backgroundColor: { default: background, ":hover": surface, [forced]: "ButtonFace" },
96
- borderColor: { default: line, [forced]: "ButtonText" },
97
- borderRadius: "0.5rem",
98
- borderStyle: "solid",
99
- borderWidth: "1px",
100
- boxSizing: "border-box",
101
- color: { default: ink, [forced]: "ButtonText" },
102
- cursor: "pointer",
103
- display: { default: "inline-flex", [noScript]: "none" },
104
- flexShrink: 0,
105
- fontFamily: "inherit",
106
- fontSize: "0.8125rem",
107
- gap: "0.4rem",
108
- justifyContent: "center",
109
- lineHeight: 1,
110
- margin: 0,
111
- minBlockSize: "2.75rem",
112
- outline: { default: "none", ":focus-visible": `2px solid ${focus}` },
113
- outlineOffset: "2px",
114
- padding: "0.65rem 0.8rem",
115
- },
116
92
  copyOverlay: {
117
93
  insetBlockEnd: "0.5rem",
118
94
  insetInlineEnd: "0.5rem",
119
95
  position: "absolute",
120
96
  },
121
- copyIcon: { blockSize: "1rem", flexShrink: 0, inlineSize: "1rem" },
122
97
  targets: { display: "grid", gap: "0.6rem", minInlineSize: 0 },
123
98
  targetsLabel: { color: { default: muted, [forced]: "CanvasText" }, fontSize: "0.8125rem", fontWeight: 500, margin: 0 },
124
99
  targetList: { display: "grid", gap: "0.45rem", gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 10rem), 1fr))", listStyle: "none", margin: 0, padding: 0 },
100
+ targetGrid: {
101
+ gridTemplateColumns: "repeat(var(--_hraness-agent-target-wide-columns, var(--_hraness-agent-target-columns, 1)), minmax(0, 1fr))",
102
+ "--_hraness-agent-target-columns": { default: null, "@container hraness-agent-setup (min-width: 20rem)": "2" },
103
+ "--_hraness-agent-target-wide-columns": { default: null, "@container hraness-agent-setup (min-width: 34rem)": "3" },
104
+ },
125
105
  target: {
126
106
  alignItems: "center",
127
107
  backgroundColor: { default: surface, ":hover": background, [forced]: "ButtonFace" },
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useId, useRef, useState, type KeyboardEvent, ty
5
5
  import type { ProviderMarkDescriptor } from "../provider-marks.js";
6
6
  import { agentSetupClassName as cx } from "./agent-setup-prompt.stylex.js";
7
7
  import { ProviderMark } from "./provider-mark.js";
8
+ import { SetupCopyButton } from "./setup-copy-button.js";
8
9
  import { SyntaxCode } from "./syntax-code.js";
9
10
 
10
11
  export interface AgentSetupTarget {
@@ -20,6 +21,8 @@ export interface AgentSetupPromptProps {
20
21
  readonly prompt: string;
21
22
  readonly label?: string;
22
23
  readonly targets?: readonly AgentSetupTarget[];
24
+ /** Keep provider actions below the prompt, or place them beside it in wide containers. */
25
+ readonly targetsPlacement?: "responsive" | "below";
23
26
  readonly className?: string;
24
27
  /** Runs once after a successful copy; callback failures do not change copy feedback. */
25
28
  readonly onCopied?: () => void;
@@ -186,21 +189,8 @@ function useCopy(text: string, subject: string, fallback: () => HTMLElement | nu
186
189
  return { state, message, copy };
187
190
  }
188
191
 
189
- function CopyGlyph({ copied }: Readonly<{ copied: boolean }>) {
190
- return (
191
- <svg aria-hidden="true" className={cx(["copyIcon"])} fill="none" focusable="false" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} viewBox="0 0 24 24">
192
- {copied ? <path d="M5 12.5l4.5 4.5L19 7.5" /> : <><rect height="12" rx="2" width="12" x="8" y="8" /><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" /></>}
193
- </svg>
194
- );
195
- }
196
-
197
192
  function CopyButton({ copy, overlay = false, state, subject }: Readonly<{ copy: () => Promise<boolean>; overlay?: boolean; state: CopyState; subject: string }>) {
198
- return (
199
- <button aria-busy={state === "copying" || undefined} aria-label={`${state === "copied" ? "Copied" : "Copy"} ${subject}`} className={cx(["copy", overlay && "copyOverlay"])} data-copy-state={state} disabled={state === "copying"} onClick={() => { void copy(); }} type="button">
200
- <CopyGlyph copied={state === "copied"} />
201
- <span>{state === "copied" ? "Copied" : state === "copying" ? "Copying" : state === "failed" ? "Copy failed" : "Copy"}</span>
202
- </button>
203
- );
193
+ return <SetupCopyButton className={cx([overlay && "copyOverlay"], "hraness-agent-setup__copy")} iconClassName="hraness-agent-setup__copyIcon" onCopy={() => { void copy(); }} state={state} subject={subject} />;
204
194
  }
205
195
 
206
196
  function CopyStatus({ children }: Readonly<{ children: ReactNode }>) {
@@ -231,10 +221,11 @@ function reserveTargetTab(owner: Window | null): Window | null {
231
221
  }
232
222
 
233
223
  /** A quiet prompt preview with the complete source behind a native disclosure. */
234
- export function AgentSetupPrompt({ prompt, label = "Agent setup", targets = [], className, onCopied }: AgentSetupPromptProps) {
224
+ export function AgentSetupPrompt({ prompt, label = "Agent setup", targets = [], targetsPlacement = "responsive", className, onCopied }: AgentSetupPromptProps) {
235
225
  assertText(prompt, "Agent setup prompt");
236
226
  assertText(label, "Agent setup label");
237
227
  assertEntries(targets, "Agent setup target");
228
+ if (targetsPlacement !== "responsive" && targetsPlacement !== "below") throw new RangeError("Unsupported agent setup targets placement.");
238
229
  for (const target of targets) {
239
230
  assertHref(target.href);
240
231
  if (target.mode !== undefined && target.mode !== "prefill" && target.mode !== "copy-and-open") throw new RangeError("Unsupported agent setup target mode.");
@@ -313,8 +304,8 @@ export function AgentSetupPrompt({ prompt, label = "Agent setup", targets = [],
313
304
  }, []);
314
305
 
315
306
  return (
316
- <section aria-label={label} className={cx(["root"], className)} data-hraness-agent-setup-prompt="">
317
- <div className={cx(["layout", targets.length > 0 && "withTargets"])}>
307
+ <section aria-label={label} className={cx(["root"], className)} data-hraness-agent-setup-prompt="" data-targets-placement={targetsPlacement}>
308
+ <div className={cx(["layout", targets.length > 0 && targetsPlacement === "responsive" && "withTargets"])}>
318
309
  <div className={cx(["frame"])} data-copy-state={state}>
319
310
  <div className={cx(["preview"])} hidden={expanded}>
320
311
  <pre aria-label={`${label} preview`} className={cx(["pre", "previewText"])}>{prompt}</pre>
@@ -330,7 +321,7 @@ export function AgentSetupPrompt({ prompt, label = "Agent setup", targets = [],
330
321
  {targets.length === 0 ? null : (
331
322
  <aside aria-labelledby={`${id}-targets`} className={cx(["targets"])}>
332
323
  <p className={cx(["targetsLabel"])} id={`${id}-targets`}>Open in</p>
333
- <ul className={cx(["targetList"])}>
324
+ <ul className={cx(["targetList", targetsPlacement === "below" && "targetGrid"])}>
334
325
  {targets.map((target) => (
335
326
  <li key={target.id}>
336
327
  <a aria-busy={target.mode === "copy-and-open" && state === "copying" || undefined} aria-disabled={target.mode === "copy-and-open" && state === "copying" || undefined} aria-label={target.mode === "copy-and-open" ? `Copy prompt and open ${target.label}` : undefined} className={cx(["target"])} data-agent-target={target.id} data-agent-target-mode={target.mode ?? "prefill"} href={target.href} onAuxClick={target.mode === "copy-and-open" ? (event) => { copyAndOpen(event, target); } : undefined} onClick={target.mode === "copy-and-open" ? (event) => { copyAndOpen(event, target); } : undefined} rel="noopener noreferrer" target="_blank">
@@ -512,10 +512,10 @@ export type ComparisonRow = Readonly<{
512
512
 
513
513
  const COMPARISON_TEXT = { yes: "Yes", no: "No", partial: "Partly" } as const;
514
514
 
515
- function ComparisonGlyph({ kind }: Readonly<{ kind: "yes" | "no" | "partial" }>) {
515
+ export function ComparisonGlyph({ kind, className }: Readonly<{ kind: "yes" | "no" | "partial"; className?: string }>) {
516
516
  const path = kind === "yes" ? "M3.5 8.5l3 3 6-7" : kind === "no" ? "M4.5 4.5l7 7m0-7l-7 7" : "M4 8h8";
517
517
  return (
518
- <svg aria-hidden="true" className="plain-publication__comparison-glyph" focusable="false" height="16" viewBox="0 0 16 16" width="16">
518
+ <svg aria-hidden="true" className={joinClasses("plain-publication__comparison-glyph", className)} focusable="false" height="16" viewBox="0 0 16 16" width="16">
519
519
  <path d={path} fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.75" />
520
520
  </svg>
521
521
  );
@@ -1,5 +1,8 @@
1
1
  "use client";
2
2
 
3
+ import { MarketingAccount, MarketingAccountActions } from "./marketing-account.js";
4
+ import { MarketingComparison } from "./marketing-comparison.js";
5
+ import { MarketingDiagram, DiagramArrowhead } from "./marketing-diagram.js";
3
6
  import {
4
7
  Badge,
5
8
  Button,
@@ -343,6 +346,7 @@ export function DesignSystemGallery({
343
346
  />
344
347
  <MarketingPillars
345
348
  ariaLabel="Relay in three points"
349
+ presentation="benefits"
346
350
  columns={3}
347
351
  pillars={[
348
352
  { label: "No hosted service", summary: "Jobs run on your machine and never wait on a server." },
@@ -397,6 +401,7 @@ export function DesignSystemGallery({
397
401
  <MarketingSection heading="Ask your agent to set it up." headingId="gallery-agent-setup" headingLevel={3} label="Agent setup">
398
402
  <AgentSetupPrompt
399
403
  label="Relay agent setup"
404
+ targetsPlacement="below"
400
405
  prompt={"Install Relay from https://relay.example/install.sh.\nRead the project instructions before making changes.\nRun relay init in this workspace and create a job named sample.\nRun the job, then show me its log."}
401
406
  targets={[
402
407
  { href: "https://claude.ai/new", id: "claude", label: "Claude", mark: "claudecode", mode: "copy-and-open" },
@@ -523,41 +528,64 @@ export function DesignSystemGallery({
523
528
  columns={2}
524
529
  groups={[
525
530
  {
526
- heading: "Sibling tools",
531
+ heading: "Relationships",
532
+ tone: "rose",
527
533
  headingId: "design-gallery-related-tools",
528
534
  items: [
529
535
  {
530
536
  href: "#gallery-install",
531
537
  name: "Ledger",
538
+ domain: "ledger.example",
532
539
  role: "Long-term storage for run logs",
533
540
  },
534
541
  {
535
542
  href: "#marketing",
536
543
  name: "Index",
544
+ domain: "index.example",
537
545
  role: "A local search index",
538
546
  },
539
547
  ],
540
548
  },
541
549
  {
542
550
  columns: 1,
543
- heading: "Shared infrastructure",
551
+ heading: "Knowledge",
552
+ tone: "indigo",
544
553
  headingId: "design-gallery-related-infra",
545
554
  items: [
546
555
  {
547
556
  href: "#marketing",
548
557
  name: "Relay",
558
+ domain: "relay.example",
549
559
  role: "The shared job runner",
550
560
  },
551
561
  ],
552
- summary: "The runner the other tools depend on.",
553
562
  },
554
563
  ]}
555
- heading="Related tools."
564
+ heading="Other tools from our studio"
556
565
  headingId="design-gallery-related-title"
557
566
  headingLevel={3}
558
- label="Related"
559
- summary="Each is a separate release. Its card says how it works with Relay."
560
567
  />
568
+ <MarketingDiagram width={720} height={216} label="Your sources stay on your device; your chosen agent reads the local workspace.">
569
+ <defs><DiagramArrowhead id="gallery-diagram-arrow" /></defs>
570
+ <rect className="hraness-diagram__node" x={24} y={40} width={276} height={136} rx={12} />
571
+ <rect className="hraness-diagram__node" x={420} y={40} width={276} height={136} rx={12} />
572
+ <path className="hraness-diagram__icon" d="M48 66h18v14H48z M53 84h8 M57 80v4" />
573
+ <text className="hraness-diagram__label" x={48} y={116}>Your sources</text>
574
+ <text className="hraness-diagram__detail" x={48} y={143}>Stored on your device</text>
575
+ <path className="hraness-diagram__connector" d="M320 108H400" markerEnd="url(#gallery-diagram-arrow)" />
576
+ <path className="hraness-diagram__icon" d="M444 73h18 M453 64v18 M447 67l12 12 M459 67l-12 12" />
577
+ <text className="hraness-diagram__label" x={444} y={116}>Your chosen agent</text>
578
+ <text className="hraness-diagram__detail" x={444} y={143}>Reads the local workspace</text>
579
+ </MarketingDiagram>
580
+ <MarketingComparison caption="Choose where your work lives" options={[{ name: "Relay" }, { name: "Hosted workspace" }]} highlight={0} rows={[
581
+ { label: "Works offline", values: [true, false] },
582
+ { label: "Bring your model", values: [true, { status: "optional", label: "On some plans" }] },
583
+ { label: "Local storage", values: [true, { status: "depends", detail: "Check the provider's export format." }] },
584
+ { label: "Cost", values: ["Free locally", "Subscription"] },
585
+ ]} note="An illustrative comparison. Product pages supply current facts and sources." />
586
+ <MarketingAccount id="gallery-account" summary="Keep your preferences and shared work together.">
587
+ <MarketingAccountActions primary={{ href: "#gallery-account", label: "Create account" }} signIn={{ href: "#gallery-account" }} />
588
+ </MarketingAccount>
561
589
  <MarketingMaker
562
590
  heading="Who builds Relay"
563
591
  headingId="design-gallery-maker-title"
@@ -26,6 +26,8 @@ export * from "./procedural-recipe.js";
26
26
  export * from "./relative-time.js";
27
27
  export * from "./production-data-preview-notice.js";
28
28
  export * from "./product-marketing.js";
29
+ export * from "./marketing-account.js";
30
+ export * from "./marketing-comparison.js";
29
31
  export * from "./route-state.js";
30
32
  export * from "./sticky-offset.js";
31
33
  export * from "./surfaces.js";
@@ -37,3 +39,4 @@ export * from "./launch-beats.js";
37
39
  export * from "./social-kit-panel.js";
38
40
  export * from "./platform-icons.js";
39
41
  export * from "./platform-install.js";
42
+ export * from "./marketing-diagram.js";