@hraness/design-kit 0.34.0 → 0.35.0

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 (44) hide show
  1. package/README.md +16 -2
  2. package/dist/{chunk-mr1vdcjq.js → chunk-8xfssb9c.js} +6 -3
  3. package/dist/{chunk-nwpcjm48.js → chunk-c9mhn3yb.js} +1115 -441
  4. package/dist/{chunk-h4k7yv6x.js → chunk-fjf81bww.js} +28 -2
  5. package/dist/chunk-gmea6p0d.js +13 -0
  6. package/dist/index.js +5 -1
  7. package/dist/mockups/client.js +50 -15
  8. package/dist/portfolio.js +586 -85
  9. package/dist/react/index.js +137 -13
  10. package/dist/react/platform-install.js +3 -2
  11. package/dist/react/server.js +17 -3
  12. package/dist/stylex-manifest.json +1 -1
  13. package/dist/stylex.css +350 -32
  14. package/package.json +14 -3
  15. package/portfolio-inventory.json +1 -1
  16. package/src/diagrams.ts +11 -0
  17. package/src/index.ts +2 -0
  18. package/src/marketing-comparison.css +162 -0
  19. package/src/marketing-diagram.css +24 -0
  20. package/src/mockups/client.tsx +25 -17
  21. package/src/mockups.css +19 -0
  22. package/src/plain-publication.css +9 -6
  23. package/src/portfolio.generated.json +578 -86
  24. package/src/portfolio.generated.ts +578 -86
  25. package/src/portfolio.ts +73 -0
  26. package/src/product-marketing-foundation.css +29 -0
  27. package/src/product-marketing.css +133 -38
  28. package/src/react/article.tsx +2 -2
  29. package/src/react/design-gallery.tsx +33 -6
  30. package/src/react/index.ts +3 -0
  31. package/src/react/marketing-account.stylex.ts +74 -0
  32. package/src/react/marketing-account.tsx +69 -0
  33. package/src/react/marketing-comparison.stylex.ts +24 -0
  34. package/src/react/marketing-comparison.tsx +73 -0
  35. package/src/react/marketing-diagram.stylex.ts +8 -0
  36. package/src/react/marketing-diagram.tsx +33 -0
  37. package/src/react/platform-install.stylex.ts +2 -1
  38. package/src/react/platform-install.tsx +2 -1
  39. package/src/react/product-marketing.stylex.ts +138 -109
  40. package/src/react/product-marketing.tsx +69 -43
  41. package/src/react/server.ts +3 -0
  42. package/src/styles.css +2 -0
  43. package/src/typography.css +32 -0
  44. /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;
@@ -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." },
@@ -523,41 +527,64 @@ export function DesignSystemGallery({
523
527
  columns={2}
524
528
  groups={[
525
529
  {
526
- heading: "Sibling tools",
530
+ heading: "Relationships",
531
+ tone: "rose",
527
532
  headingId: "design-gallery-related-tools",
528
533
  items: [
529
534
  {
530
535
  href: "#gallery-install",
531
536
  name: "Ledger",
537
+ domain: "ledger.example",
532
538
  role: "Long-term storage for run logs",
533
539
  },
534
540
  {
535
541
  href: "#marketing",
536
542
  name: "Index",
543
+ domain: "index.example",
537
544
  role: "A local search index",
538
545
  },
539
546
  ],
540
547
  },
541
548
  {
542
549
  columns: 1,
543
- heading: "Shared infrastructure",
550
+ heading: "Knowledge",
551
+ tone: "indigo",
544
552
  headingId: "design-gallery-related-infra",
545
553
  items: [
546
554
  {
547
555
  href: "#marketing",
548
556
  name: "Relay",
557
+ domain: "relay.example",
549
558
  role: "The shared job runner",
550
559
  },
551
560
  ],
552
- summary: "The runner the other tools depend on.",
553
561
  },
554
562
  ]}
555
- heading="Related tools."
563
+ heading="Other tools from our studio"
556
564
  headingId="design-gallery-related-title"
557
565
  headingLevel={3}
558
- label="Related"
559
- summary="Each is a separate release. Its card says how it works with Relay."
560
566
  />
567
+ <MarketingDiagram width={720} height={216} label="Your sources stay on your device; your chosen agent reads the local workspace.">
568
+ <defs><DiagramArrowhead id="gallery-diagram-arrow" /></defs>
569
+ <rect className="hraness-diagram__node" x={24} y={40} width={276} height={136} rx={12} />
570
+ <rect className="hraness-diagram__node" x={420} y={40} width={276} height={136} rx={12} />
571
+ <path className="hraness-diagram__icon" d="M48 66h18v14H48z M53 84h8 M57 80v4" />
572
+ <text className="hraness-diagram__label" x={48} y={116}>Your sources</text>
573
+ <text className="hraness-diagram__detail" x={48} y={143}>Stored on your device</text>
574
+ <path className="hraness-diagram__connector" d="M320 108H400" markerEnd="url(#gallery-diagram-arrow)" />
575
+ <path className="hraness-diagram__icon" d="M444 73h18 M453 64v18 M447 67l12 12 M459 67l-12 12" />
576
+ <text className="hraness-diagram__label" x={444} y={116}>Your chosen agent</text>
577
+ <text className="hraness-diagram__detail" x={444} y={143}>Reads the local workspace</text>
578
+ </MarketingDiagram>
579
+ <MarketingComparison caption="Choose where your work lives" options={[{ name: "Relay" }, { name: "Hosted workspace" }]} highlight={0} rows={[
580
+ { label: "Works offline", values: [true, false] },
581
+ { label: "Bring your model", values: [true, { status: "optional", label: "On some plans" }] },
582
+ { label: "Local storage", values: [true, { status: "depends", detail: "Check the provider's export format." }] },
583
+ { label: "Cost", values: ["Free locally", "Subscription"] },
584
+ ]} note="An illustrative comparison. Product pages supply current facts and sources." />
585
+ <MarketingAccount id="gallery-account" summary="Keep your preferences and shared work together.">
586
+ <MarketingAccountActions primary={{ href: "#gallery-account", label: "Create account" }} signIn={{ href: "#gallery-account" }} />
587
+ </MarketingAccount>
561
588
  <MarketingMaker
562
589
  heading="Who builds Relay"
563
590
  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";
@@ -0,0 +1,74 @@
1
+ import * as stylex from "@stylexjs/stylex";
2
+
3
+ export const marketingAccountStyles = stylex.create({
4
+ section: {
5
+ alignItems: "center",
6
+ "border-top": "var(--hraness-marketing-rule, 1px solid var(--line))",
7
+ color: "var(--foreground)",
8
+ display: "grid",
9
+ gap: "2rem 4rem",
10
+ gridTemplateColumns: { default: "minmax(0, 1fr)", "@media (min-width: 50rem)": "minmax(0, 1fr) minmax(0, 1fr)" },
11
+ paddingBlock: "clamp(2.5rem, 6vw, 4rem)",
12
+ scrollMarginTop: "calc(var(--hraness-sticky-offset, 5rem) + 2rem)",
13
+ },
14
+ copy: { minWidth: 0 },
15
+ heading: {
16
+ fontFamily: "var(--font-text)",
17
+ fontSize: "clamp(1.625rem, 1.3rem + 1vw, 2.25rem)",
18
+ fontWeight: 550,
19
+ letterSpacing: "-0.02em",
20
+ lineHeight: 1.15,
21
+ margin: 0,
22
+ textWrap: "balance",
23
+ },
24
+ summary: {
25
+ color: "var(--muted)",
26
+ fontSize: "1rem",
27
+ lineHeight: 1.6,
28
+ marginTop: "1rem",
29
+ maxWidth: "42ch",
30
+ textWrap: "pretty",
31
+ },
32
+ content: { minWidth: 0 },
33
+ actions: { alignItems: "center", display: "flex", flexWrap: "wrap", gap: "0.75rem 1.5rem" },
34
+ primary: {
35
+ alignItems: "center",
36
+ backgroundColor: { default: "var(--primary)", ":hover": "color-mix(in oklch, var(--primary) 88%, var(--foreground))", "@media (forced-colors: active)": "ButtonFace" },
37
+ borderWidth: "1px",
38
+ borderStyle: "solid",
39
+ borderColor: { default: "transparent", "@media (forced-colors: active)": "ButtonText" },
40
+ borderRadius: "0.625rem",
41
+ color: { default: "var(--primary-foreground)", "@media (forced-colors: active)": "ButtonText" },
42
+ display: "inline-flex",
43
+ fontFamily: "var(--font-text)",
44
+ fontSize: "1.0625rem",
45
+ fontWeight: 550,
46
+ justifyContent: "center",
47
+ lineHeight: 1.3,
48
+ maxWidth: "100%",
49
+ minHeight: "3.5rem",
50
+ outline: { default: null, ":focus-visible": "2px solid var(--primary)" },
51
+ outlineOffset: { default: null, ":focus-visible": "3px" },
52
+ paddingBlock: "0.875rem",
53
+ paddingInline: "1.5rem",
54
+ textAlign: "center",
55
+ textDecoration: "none",
56
+ },
57
+ signIn: {
58
+ alignItems: "center",
59
+ color: "var(--foreground)",
60
+ display: "inline-flex",
61
+ fontFamily: "var(--font-text)",
62
+ fontSize: "1.0625rem",
63
+ fontWeight: 450,
64
+ lineHeight: 1.3,
65
+ minHeight: "3.5rem",
66
+ outline: { default: null, ":focus-visible": "2px solid var(--primary)" },
67
+ outlineOffset: { default: null, ":focus-visible": "3px" },
68
+ textDecorationLine: "underline",
69
+ textDecorationStyle: "dotted",
70
+ textDecorationColor: { default: "color-mix(in srgb, currentColor 45%, transparent)", ":hover": "currentColor", "@media (forced-colors: active)": "LinkText" },
71
+ textDecorationThickness: "1px",
72
+ textUnderlineOffset: "0.22em",
73
+ },
74
+ });
@@ -0,0 +1,69 @@
1
+ import * as stylex from "@stylexjs/stylex";
2
+ import type { ReactNode } from "react";
3
+
4
+ import { marketingAccountStyles as styles } from "./marketing-account.stylex.js";
5
+
6
+ /** Optional account access near the end of a product's marketing page. */
7
+ export function MarketingAccount({
8
+ children,
9
+ className,
10
+ heading = "Your account",
11
+ id = "account",
12
+ summary,
13
+ }: Readonly<{
14
+ children: ReactNode;
15
+ className?: string;
16
+ heading?: string;
17
+ id?: string;
18
+ summary: ReactNode;
19
+ }>) {
20
+ const presentation = stylex.props(styles.section);
21
+ return (
22
+ <section
23
+ {...presentation}
24
+ aria-labelledby={`${id}-heading`}
25
+ className={["hraness-marketing-account", presentation.className, className].filter(Boolean).join(" ")}
26
+ id={id}
27
+ >
28
+ <div {...stylex.props(styles.copy)}>
29
+ <h2 {...stylex.props(styles.heading)} id={`${id}-heading`}>{heading}</h2>
30
+ <div {...stylex.props(styles.summary)}>{summary}</div>
31
+ </div>
32
+ <div {...stylex.props(styles.content)}>{children}</div>
33
+ </section>
34
+ );
35
+ }
36
+
37
+ export interface MarketingAccountAction {
38
+ readonly href: string;
39
+ readonly analyticsEvent?: string;
40
+ readonly analyticsId?: string;
41
+ }
42
+
43
+ /** One prominent account action and an optional, equally tall sign-in link. */
44
+ export function MarketingAccountActions({ primary, signIn }: Readonly<{
45
+ primary: MarketingAccountAction & Readonly<{ label: string }>;
46
+ signIn?: MarketingAccountAction;
47
+ }>) {
48
+ return (
49
+ <div {...stylex.props(styles.actions)} className={`hraness-marketing-account__actions ${stylex.props(styles.actions).className}`}>
50
+ <a
51
+ {...stylex.props(styles.primary)}
52
+ className={`hraness-marketing-account__primary ${stylex.props(styles.primary).className}`}
53
+ data-analytics-event={primary.analyticsEvent}
54
+ data-analytics-id={primary.analyticsId}
55
+ data-emphasis="primary"
56
+ href={primary.href}
57
+ >{primary.label}</a>
58
+ {signIn === undefined ? null : (
59
+ <a
60
+ {...stylex.props(styles.signIn)}
61
+ className={`hraness-marketing-account__sign-in ${stylex.props(styles.signIn).className}`}
62
+ data-analytics-event={signIn.analyticsEvent}
63
+ data-analytics-id={signIn.analyticsId}
64
+ href={signIn.href}
65
+ >Sign in</a>
66
+ )}
67
+ </div>
68
+ );
69
+ }