@godxjp/ui 28.7.0 → 28.9.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 (119) hide show
  1. package/dist/components/data-display/index.d.ts +2 -0
  2. package/dist/components/data-display/index.js +2 -0
  3. package/dist/components/data-display/marquee.d.ts +16 -0
  4. package/dist/components/data-display/marquee.js +155 -0
  5. package/dist/components/general/reveal.d.ts +23 -2
  6. package/dist/components/general/reveal.js +37 -7
  7. package/dist/components/general/typography.d.ts +4 -1
  8. package/dist/components/general/typography.js +14 -1
  9. package/dist/components/layout/affix.d.ts +86 -0
  10. package/dist/components/layout/affix.js +187 -0
  11. package/dist/components/layout/index.d.ts +4 -0
  12. package/dist/components/layout/index.js +4 -0
  13. package/dist/components/layout/legal-document-shell.js +4 -3
  14. package/dist/components/layout/masonry.d.ts +74 -0
  15. package/dist/components/layout/masonry.js +214 -0
  16. package/dist/components/layout/page-container.js +5 -20
  17. package/dist/components/navigation/anchor.d.ts +64 -0
  18. package/dist/components/navigation/anchor.js +284 -0
  19. package/dist/components/navigation/index.d.ts +4 -0
  20. package/dist/components/navigation/index.js +4 -0
  21. package/dist/components/navigation/mega-menu.d.ts +21 -0
  22. package/dist/components/navigation/mega-menu.js +526 -0
  23. package/dist/contracts/measurement.json +1 -1
  24. package/dist/i18n/messages/en.json +517 -0
  25. package/dist/i18n/messages/ja.json +513 -0
  26. package/dist/i18n/messages/vi.json +513 -0
  27. package/dist/lib/hooks.d.ts +68 -0
  28. package/dist/lib/hooks.js +52 -0
  29. package/dist/lib/platform.d.ts +14 -0
  30. package/dist/lib/platform.js +10 -1
  31. package/dist/lib/utils.d.ts +1 -1
  32. package/dist/lib/utils.js +3 -2
  33. package/dist/lib/variants.js +4 -1
  34. package/dist/props/components/data-display.prop.d.ts +95 -1
  35. package/dist/props/components/general.prop.d.ts +47 -3
  36. package/dist/props/components/layout.prop.d.ts +194 -0
  37. package/dist/props/components/navigation.prop.d.ts +263 -0
  38. package/dist/props/registry.d.ts +359 -4
  39. package/dist/props/registry.js +472 -3
  40. package/dist/props/vocabulary/index.d.ts +1 -1
  41. package/dist/props/vocabulary/interaction.prop.d.ts +39 -2
  42. package/dist/props/vocabulary/layout.prop.d.ts +1 -1
  43. package/dist/styles/base.css +47 -14
  44. package/dist/styles/card-layout.css +2 -2
  45. package/dist/styles/chart-layout.css +6 -6
  46. package/dist/styles/control.css +15 -10
  47. package/dist/styles/data-display-layout.css +22 -6
  48. package/dist/styles/density.css +6 -0
  49. package/dist/styles/dialog-layout.css +4 -1
  50. package/dist/styles/focus-ring.css +4 -1
  51. package/dist/styles/layout.css +92 -3
  52. package/dist/styles/motion.css +121 -1
  53. package/dist/styles/navigation-layout.css +397 -1
  54. package/dist/styles/shell-layout.css +28 -21
  55. package/dist/styles/text-layout.css +134 -15
  56. package/dist/tokens/base.css +5 -0
  57. package/dist/tokens/components/activity.css +13 -4
  58. package/dist/tokens/components/affix.css +7 -0
  59. package/dist/tokens/components/anchor.css +17 -0
  60. package/dist/tokens/components/attachments.css +1 -1
  61. package/dist/tokens/components/badge.css +1 -1
  62. package/dist/tokens/components/card.css +28 -7
  63. package/dist/tokens/components/chart.css +4 -1
  64. package/dist/tokens/components/chat-composer.css +4 -1
  65. package/dist/tokens/components/control.css +72 -33
  66. package/dist/tokens/components/conversations.css +4 -1
  67. package/dist/tokens/components/data-display.css +42 -15
  68. package/dist/tokens/components/data-entry.css +8 -2
  69. package/dist/tokens/components/descriptions.css +1 -1
  70. package/dist/tokens/components/feedback.css +8 -5
  71. package/dist/tokens/components/float-button.css +8 -2
  72. package/dist/tokens/components/form.css +1 -1
  73. package/dist/tokens/components/legal-document.css +12 -3
  74. package/dist/tokens/components/logo.css +15 -6
  75. package/dist/tokens/components/marquee.css +7 -0
  76. package/dist/tokens/components/masonry.css +6 -0
  77. package/dist/tokens/components/mega-menu.css +71 -0
  78. package/dist/tokens/components/navigation.css +37 -13
  79. package/dist/tokens/components/segmented.css +4 -1
  80. package/dist/tokens/components/separator.css +4 -1
  81. package/dist/tokens/components/shell.css +99 -31
  82. package/dist/tokens/components/table.css +11 -5
  83. package/dist/tokens/components/thought-chain.css +4 -1
  84. package/dist/tokens/components/toggle.css +4 -1
  85. package/dist/tokens/components/tree.css +1 -1
  86. package/dist/tokens/components/upload.css +21 -9
  87. package/dist/tokens/foundation.css +35 -30
  88. package/dist/tokens/semantic/layout.css +26 -5
  89. package/docs/COMPOSITION-VS-COMPONENT.md +19 -1
  90. package/docs/DESIGN-AUTHORITY.md +99 -18
  91. package/docs/FRAME-COVERAGE-REPORT.md +7 -2
  92. package/docs/TOKENS.md +16 -1
  93. package/docs/data-display/marquee.tsx +254 -0
  94. package/docs/foundation/_theme-editor-scope.ts +222 -0
  95. package/docs/foundation/density.tsx +12 -2
  96. package/docs/foundation/spacing.tsx +5 -0
  97. package/docs/foundation/theme-editor.tsx +645 -0
  98. package/docs/general/activity.tsx +65 -0
  99. package/docs/general/reveal.tsx +290 -22
  100. package/docs/general/typography.tsx +91 -1
  101. package/docs/layout/affix.tsx +209 -0
  102. package/docs/layout/masonry.tsx +291 -0
  103. package/docs/navigation/anchor.tsx +285 -0
  104. package/docs/navigation/mega-menu-panel.tsx +86 -0
  105. package/docs/navigation/mega-menu.tsx +254 -0
  106. package/docs/roadmap/website-components.md +779 -0
  107. package/docs/showcase/acme-website.tsx +75 -39
  108. package/docs/showcase/case4-login.tsx +10 -2
  109. package/docs/showcase/case5-shift-calendar.tsx +1 -1
  110. package/docs/showcase/case6-agency-handy.tsx +6 -6
  111. package/docs/showcase/futurelastic-web.tsx +89 -49
  112. package/docs/showcase/marketing-page.tsx +885 -0
  113. package/docs/showcase/table-expandable-rows.tsx +4 -1
  114. package/docs/showcase/table-footer-totals.tsx +12 -2
  115. package/docs/showcase/theme-customization.tsx +1259 -0
  116. package/package.json +6 -3
  117. package/scripts/brand-accent.generated.mjs +27 -0
  118. package/scripts/ui-audit.mjs +66 -0
  119. package/scripts/visual-audit-rules.mjs +46 -2
@@ -32,7 +32,7 @@ import {
32
32
  Warehouse,
33
33
  } from "lucide-react";
34
34
 
35
- import { Button, Text } from "@godxjp/ui/general";
35
+ import { Button, Heading, Icon, Text } from "@godxjp/ui/general";
36
36
  import { Avatar, AvatarFallback, Card, CardContent, CardCover } from "@godxjp/ui/data-display";
37
37
  import { Flex, ResponsiveGrid } from "@godxjp/ui/layout";
38
38
 
@@ -52,6 +52,7 @@ const THEME = `
52
52
  3.18 on --muted. It was 216 16% 80% = 1.46:1. --border is decorative chrome; it stays. */
53
53
  --border: 214 27% 90%; --input: 216 16% 55%;
54
54
  --success: 152 100% 25%; --warning: 41 73% 53%; --destructive: 11 82% 56%; --info: 203 100% 37%;
55
+ --letter-spacing-tight: -0.02em; /* the display tracking .tx-display/.tx-h2 hand-wrote, once */
55
56
  --radius: 0.875rem; --radius-md: 10px; --radius-lg: 14px;
56
57
  --card-radius: var(--radius); --control-radius: var(--radius);
57
58
  --shadow-color: 38 79 145;
@@ -62,33 +63,44 @@ const THEME = `
62
63
  --font-family-body: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
63
64
  --font-family-sans: var(--font-family-body);
64
65
  --text-brand: 41 84% 26%; /* dark gold, eyebrows clear AA on light sections */
66
+ /* The glass bar, opted into once (gh#831). These two used to be a literal 0.85 alpha and a
67
+ literal 10px blur on .tx-navbar. futurelastic-web.tsx hand-wrote the identical pair. */
68
+ --topbar-background-alpha: 85%;
69
+ --topbar-backdrop-blur-size: 10px;
65
70
  }
66
71
  /* Marketing layout + type (a consumer's section stylesheet) */
67
- [data-tenant="acme-web"] .tx-shell { margin-inline: auto; width: 100%; max-width: 1200px; padding-inline: 2rem;
72
+ [data-tenant="acme-web"] .tx-shell { margin-inline: auto; width: 100%; max-inline-size: var(--page-measure-wide);
73
+ padding-inline: var(--space-8);
68
74
  /* ResponsiveGrid uses @container queries, so establish the container here (PageContainer does this
69
75
  * in an app; a marketing composition provides it on its own shell). */
70
76
  container-type: inline-size; }
71
- [data-tenant="acme-web"] .tx-section { padding-block: 5rem; }
72
- [data-tenant="acme-web"] .tx-navbar { position: sticky; top: 0; z-index: 30; background: hsl(var(--background) / 0.85);
73
- backdrop-filter: blur(10px); border-bottom: 1px solid hsl(var(--border)); }
77
+ [data-tenant="acme-web"] .tx-section { padding-block: var(--space-section-band); }
78
+ /* The sticky header IS this page's bar region, so it reads the --topbar-* glass knobs. It is not a
79
+ <Topbar> for one reason: the glass must span the full bleed while the bar's CONTENT stays on the
80
+ .tx-shell measure, and Topbar is a single flex row with no inner shell. */
81
+ [data-tenant="acme-web"] .tx-navbar { position: sticky; inset-block-start: 0; z-index: 30;
82
+ background-color: hsl(var(--background) / var(--topbar-background-alpha));
83
+ backdrop-filter: blur(var(--topbar-backdrop-blur-size));
84
+ border-block-end: var(--stroke-hairline) solid hsl(var(--border)); }
74
85
  [data-tenant="acme-web"] .tx-navbar-inner { height: 72px; display: flex; align-items: center; gap: 2.25rem; }
75
86
  [data-tenant="acme-web"] .tx-brand { font-family: var(--font-family-display); font-weight: 800; font-size: 1.375rem; letter-spacing: -0.01em; color: hsl(var(--foreground)); }
76
87
  [data-tenant="acme-web"] .tx-eyebrow { font-family: var(--font-family-display); font-weight: 700; font-size: 0.8125rem;
77
88
  letter-spacing: 0.12em; text-transform: uppercase; color: hsl(var(--text-brand)); }
78
- [data-tenant="acme-web"] .tx-display { font-family: var(--font-family-display); font-weight: 900; font-size: 3.5rem;
79
- line-height: 1.05; letter-spacing: -0.02em; margin: 1rem 0; color: hsl(var(--foreground)); }
80
- [data-tenant="acme-web"] .tx-h2 { font-family: var(--font-family-display); font-weight: 800; font-size: 2.25rem;
81
- line-height: 1.15; letter-spacing: -0.02em; margin: 0.75rem 0 0.625rem; color: hsl(var(--foreground)); }
82
- [data-tenant="acme-web"] .tx-lead { font-size: 1.1875rem; line-height: 1.6; color: hsl(var(--muted-foreground)); max-width: 480px; }
89
+ /* .tx-display / .tx-h2 / .tx-lead are GONE (gh#826). They existed only because the display type
90
+ ramp had tokens and no prop, so all three were a raw font-size plus the leading that has to come
91
+ with it. They are now Heading size="5xl", Heading size="4xl" and Text size="xl". The
92
+ brand face is not lost with them: base.css already points every <h1>-<h6> at
93
+ --font-family-display, which the token block above sets. The one knob those classes carried that
94
+ is genuinely brand and not scale is the display tracking, and it IS a token — set once, below. */
83
95
  /* --text-brand, not --primary (gh#643): --primary is a FILL role, and as ink it measured
84
96
  2.01:1 here. The AA-safe text tier already exists and .tx-eyebrow above already reads it. */
85
97
  [data-tenant="acme-web"] .tx-gold { color: hsl(var(--text-brand)); }
86
98
  [data-tenant="acme-web"] .tx-stat { font-family: var(--font-family-display); font-weight: 800; font-size: 1.75rem; color: hsl(var(--primary)); }
87
- [data-tenant="acme-web"] .tx-hero-grid { display: grid; gap: 3rem; align-items: center; padding-block: 6rem; }
99
+ [data-tenant="acme-web"] .tx-hero-grid { display: grid; gap: var(--space-12); align-items: center; padding-block: var(--space-section-hero); }
88
100
  @media (min-width: 1024px) { [data-tenant="acme-web"] .tx-hero-grid { grid-template-columns: 1.1fr 0.9fr; } }
89
- [data-tenant="acme-web"] .tx-glow { position: absolute; inset: 0; pointer-events: none; }
90
- [data-tenant="acme-web"] .tx-glow-tr { background: radial-gradient(40% 50% at 88% -10%, hsl(41 73% 53% / 0.22), transparent 70%); }
91
- [data-tenant="acme-web"] .tx-glow-bl { background: radial-gradient(40% 50% at 12% 120%, hsl(41 73% 53% / 0.18), transparent 70%); }
101
+ /* .tx-glow* are GONE too: .ui-brand-glow shipped with exactly this gradient and these four
102
+ knobs, and nothing in the repo referenced it while four sections hand-wrote it (gh#826). The
103
+ halo's position/size/alpha are now per-instance token overrides at the call site. */
92
104
  [data-tenant="acme-web"] .tx-field { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1rem;
93
105
  background: hsl(0 0% 100% / 0.08); border-radius: var(--radius-md); }
94
106
  [data-tenant="acme-web"] .tx-quote-total { display: flex; align-items: center; justify-content: space-between;
@@ -98,10 +110,7 @@ const THEME = `
98
110
  [data-tenant="acme-web"] .tx-footer-grid { display: grid; gap: 2.25rem; }
99
111
  @media (min-width: 768px) { [data-tenant="acme-web"] .tx-footer-grid { grid-template-columns: 1.4fr 1fr 1fr 1fr; } }
100
112
  [data-tenant="acme-web"] .tx-footer-bottom { display: flex; align-items: center; justify-content: space-between;
101
- margin-top: 2.25rem; padding-top: 1.25rem; border-top: 1px solid hsl(0 0% 100% / 0.1); }
102
- [data-tenant="acme-web"] .tx-medallion svg { width: 1.25rem; height: 1.25rem; }
103
- [data-tenant="acme-web"] .tx-icon-18 { width: 1.125rem; height: 1.125rem; }
104
- [data-tenant="acme-web"] .tx-icon-22 { width: 1.375rem; height: 1.375rem; }
113
+ margin-block-start: 2.25rem; padding-block-start: var(--space-5); border-block-start: var(--stroke-hairline) solid hsl(0 0% 100% / 0.1); }
105
114
  /* Navy region: role scoping so descendants render on-dark (white text, glass cards, outline btns). */
106
115
  [data-tenant="acme-web"] .tx-navy {
107
116
  background: hsl(217 61% 12%); --foreground: 0 0% 100%;
@@ -156,19 +165,28 @@ function Hero() {
156
165
  ];
157
166
  return (
158
167
  <section className="tx-navy relative overflow-hidden">
159
- <div className="tx-glow tx-glow-tr" />
168
+ <div
169
+ className="ui-brand-glow absolute inset-0"
170
+ style={
171
+ {
172
+ "--brand-glow-size": "40% 50%",
173
+ "--brand-glow-position": "88% -10%",
174
+ "--brand-glow-alpha": "0.22",
175
+ } as React.CSSProperties
176
+ }
177
+ />
160
178
  <div className={`${SHELL} tx-hero-grid relative`}>
161
179
  <Flex direction="col" gap="xl">
162
180
  <div className="tx-eyebrow">Mua hộ &amp; vận chuyển quốc tế</div>
163
- <h1 className="tx-display">
181
+ <Heading level={1} size="5xl" weight="bold">
164
182
  Mua sắm toàn cầu,
165
183
  <br />
166
184
  <span className="tx-gold">ACME</span> lo phần còn lại.
167
- </h1>
168
- <p className="tx-lead">
185
+ </Heading>
186
+ <Text as="p" size="xl" tone="muted">
169
187
  Đặt mua, thanh toán và vận chuyển hàng từ Nhật, Hàn, Indonesia và Mỹ về tận nhà. Minh
170
188
  bạch chi phí, theo dõi từng bước.
171
- </p>
189
+ </Text>
172
190
  <Flex direction="row" gap="md">
173
191
  <Button size="lg">
174
192
  Nhận báo giá miễn phí
@@ -196,9 +214,9 @@ function Hero() {
196
214
  <Flex direction="col" gap="md">
197
215
  <div className="tx-eyebrow">Ước tính phí vận chuyển</div>
198
216
  <Flex direction="col" gap="sm">
199
- {fields.map(([Icon, label, value]) => (
217
+ {fields.map(([Glyph, label, value]) => (
200
218
  <div key={label} className="tx-field">
201
- <Icon aria-hidden="true" className="tx-gold tx-icon-18" />
219
+ <Icon as={Glyph} size="lg" className="tx-gold" />
202
220
  <div className="min-w-0 flex-1">
203
221
  <Text as="div" size="2xs" tone="muted">
204
222
  {label}
@@ -207,7 +225,7 @@ function Hero() {
207
225
  {value}
208
226
  </Text>
209
227
  </div>
210
- <ChevronDown aria-hidden="true" className="tx-icon-18" />
228
+ <Icon as={ChevronDown} size="lg" />
211
229
  </div>
212
230
  ))}
213
231
  <div className="tx-quote-total">
@@ -241,17 +259,23 @@ function SectionHead({
241
259
  return (
242
260
  <div className={center ? "mx-auto text-center" : undefined}>
243
261
  <div className="tx-eyebrow">{eyebrow}</div>
244
- <h2 className="tx-h2">{title}</h2>
245
- {sub ? <p className="tx-lead mx-auto">{sub}</p> : null}
262
+ <Heading level={2} size="4xl" weight="bold">
263
+ {title}
264
+ </Heading>
265
+ {sub ? (
266
+ <Text as="p" size="xl" tone="muted" className="mx-auto">
267
+ {sub}
268
+ </Text>
269
+ ) : null}
246
270
  </div>
247
271
  );
248
272
  }
249
273
 
250
- function Medallion({ icon: Icon }: { icon: typeof Globe }) {
274
+ function Medallion({ icon: Glyph }: { icon: typeof Globe }) {
251
275
  return (
252
276
  <Avatar className="rounded-md">
253
- <AvatarFallback className="bg-primary/10 text-primary tx-medallion rounded-md">
254
- <Icon aria-hidden="true" />
277
+ <AvatarFallback className="bg-primary/10 text-primary rounded-md">
278
+ <Icon as={Glyph} size="lg" />
255
279
  </AvatarFallback>
256
280
  </Avatar>
257
281
  );
@@ -315,7 +339,7 @@ function Steps() {
315
339
  <Flex direction="col" gap="xl" className={SHELL}>
316
340
  <SectionHead eyebrow="Quy trình" title="Mua hàng quốc tế trong 4 bước" />
317
341
  <ResponsiveGrid columns={{ sm: 1, md: 2, lg: 4 }}>
318
- {steps.map(([Icon, t, d], i) => (
342
+ {steps.map(([Glyph, t, d], i) => (
319
343
  <Flex direction="col" gap="md" key={t}>
320
344
  <Flex direction="row" gap="sm" align="center">
321
345
  <Avatar>
@@ -323,7 +347,7 @@ function Steps() {
323
347
  {i + 1}
324
348
  </AvatarFallback>
325
349
  </Avatar>
326
- <Icon aria-hidden="true" className="text-brand tx-icon-22" />
350
+ <Icon as={Glyph} size="xl" className="text-brand" />
327
351
  </Flex>
328
352
  <Text as="div" weight="bold" size="lg">
329
353
  {t}
@@ -380,10 +404,22 @@ function Routes() {
380
404
  function CtaBanner() {
381
405
  return (
382
406
  <section className="tx-navy tx-section relative overflow-hidden">
383
- <div className="tx-glow tx-glow-bl" />
407
+ <div
408
+ className="ui-brand-glow absolute inset-0"
409
+ style={
410
+ {
411
+ "--brand-glow-size": "40% 50%",
412
+ "--brand-glow-position": "12% 120%",
413
+ } as React.CSSProperties
414
+ }
415
+ />
384
416
  <div className={`${SHELL} relative text-center`}>
385
- <h2 className="tx-h2">Sẵn sàng cho đơn hàng đầu tiên?</h2>
386
- <p className="tx-lead mx-auto">Tạo tài khoản miễn phí và nhận báo giá trong 5 phút.</p>
417
+ <Heading level={2} size="4xl" weight="bold">
418
+ Sẵn sàng cho đơn hàng đầu tiên?
419
+ </Heading>
420
+ <Text as="p" size="xl" tone="muted" className="mx-auto">
421
+ Tạo tài khoản miễn phí và nhận báo giá trong 5 phút.
422
+ </Text>
387
423
  <Flex direction="row" gap="md" justify="center">
388
424
  <Button size="lg">
389
425
  <UserPlus aria-hidden="true" />
@@ -406,7 +442,7 @@ function Footer() {
406
442
  ];
407
443
  return (
408
444
  <footer className="tx-navy tx-navy-deep">
409
- <div className={`${SHELL} tx-footer-grid`} style={{ paddingBlock: "3.5rem 2rem" }}>
445
+ <div className={`${SHELL} tx-footer-grid`} style={{ paddingBlock: "3.5rem var(--space-8)" }}>
410
446
  <Flex direction="col" gap="md">
411
447
  <span className="tx-brand">
412
448
  TIXI<span className="tx-gold">MAX</span>
@@ -438,8 +474,8 @@ function Footer() {
438
474
  2026 ACME Logistics. All rights reserved.
439
475
  </Text>
440
476
  <Flex direction="row" gap="md">
441
- {[Mail, Phone, Globe].map((Icon, i) => (
442
- <Icon key={i} aria-hidden="true" className="text-muted-foreground tx-icon-18" />
477
+ {[Mail, Phone, Globe].map((Glyph, i) => (
478
+ <Icon key={i} as={Glyph} size="lg" tone="muted" />
443
479
  ))}
444
480
  </Flex>
445
481
  </div>
@@ -283,11 +283,19 @@ export default function LoginShowcase() {
283
283
 
284
284
  <Text as="p" size="2xs" tone="muted" align="center" className="leading-relaxed">
285
285
  ログインすると{" "}
286
- <Button variant="link" size="sm" className="text-[length:var(--font-size-2xs)]">
286
+ <Button
287
+ variant="link"
288
+ size="sm"
289
+ className="text-[length:var(--font-size-2xs, calc(var(--font-size-base) / var(--font-size-ratio) / var(--font-size-ratio)))]"
290
+ >
287
291
  利用規約
288
292
  </Button>{" "}
289
293
  ·{" "}
290
- <Button variant="link" size="sm" className="text-[length:var(--font-size-2xs)]">
294
+ <Button
295
+ variant="link"
296
+ size="sm"
297
+ className="text-[length:var(--font-size-2xs, calc(var(--font-size-base) / var(--font-size-ratio) / var(--font-size-ratio)))]"
298
+ >
291
299
  プライバシーポリシー
292
300
  </Button>{" "}
293
301
  に同意したものとみなされます。
@@ -588,7 +588,7 @@ function MonthGrid({ onPick }: { onPick: (d: DayCell) => void }) {
588
588
  tone="destructive"
589
589
  variant="outline"
590
590
  shape="sharp"
591
- className="text-[length:var(--font-size-2xs)]"
591
+ className="text-[length:var(--font-size-2xs, calc(var(--font-size-base) / var(--font-size-ratio) / var(--font-size-ratio)))]"
592
592
  >
593
593
  祝
594
594
  </Badge>
@@ -368,7 +368,7 @@ function TabBar({ active, onChange }: { active: string; onChange: (id: string) =
368
368
  onClick={() => onChange(t.id)}
369
369
  aria-current={isActive ? "page" : undefined}
370
370
  className={
371
- "h-full flex-col rounded-none text-[length:var(--font-size-2xs)] font-medium " +
371
+ "text-[length:var(--font-size-2xs, calc(var(--font-size-base) / var(--font-size-ratio) / var(--font-size-ratio)))] h-full flex-col rounded-none font-medium " +
372
372
  (isActive ? "text-primary" : "text-muted-foreground")
373
373
  }
374
374
  >
@@ -637,7 +637,7 @@ function InboundTab({
637
637
  <ToggleGroupItem
638
638
  key={f.id}
639
639
  value={f.id}
640
- className="h-9 shrink-0 rounded-full text-[length:var(--font-size-xs)] whitespace-nowrap"
640
+ className="text-[length:var(--font-size-xs, calc(var(--font-size-base) / var(--font-size-ratio)))] h-9 shrink-0 rounded-full whitespace-nowrap"
641
641
  // The library's OWN counter pill, not a `Text` at 70% opacity (gh#643). The
642
642
  // hand-rolled one measured 2.15:1 on the selected chip's --primary fill, because
643
643
  // `opacity-70` blends whatever ink it inherits back towards the fill underneath it —
@@ -852,19 +852,19 @@ function OutboundTab({ seg, setSeg }: { seg: string; setSeg: (v: string) => void
852
852
  <ResponsiveGrid columns={3} gap="xs">
853
853
  <ToggleGroupItem
854
854
  value="seal"
855
- className="h-9 rounded-xl text-[length:var(--font-size-xs)] whitespace-nowrap"
855
+ className="text-[length:var(--font-size-xs, calc(var(--font-size-base) / var(--font-size-ratio)))] h-9 rounded-xl whitespace-nowrap"
856
856
  >
857
857
  Chờ niêm phong
858
858
  </ToggleGroupItem>
859
859
  <ToggleGroupItem
860
860
  value="handoff"
861
- className="h-9 rounded-xl text-[length:var(--font-size-xs)] whitespace-nowrap"
861
+ className="text-[length:var(--font-size-xs, calc(var(--font-size-base) / var(--font-size-ratio)))] h-9 rounded-xl whitespace-nowrap"
862
862
  >
863
863
  Chờ bàn giao
864
864
  </ToggleGroupItem>
865
865
  <ToggleGroupItem
866
866
  value="done"
867
- className="h-9 rounded-xl text-[length:var(--font-size-xs)] whitespace-nowrap"
867
+ className="text-[length:var(--font-size-xs, calc(var(--font-size-base) / var(--font-size-ratio)))] h-9 rounded-xl whitespace-nowrap"
868
868
  >
869
869
  Đã bàn giao
870
870
  </ToggleGroupItem>
@@ -1018,7 +1018,7 @@ export default function AgencyHandyShowcase() {
1018
1018
  <ToggleGroupItem
1019
1019
  key={s}
1020
1020
  value={s}
1021
- className="size-7 rounded-md text-[length:var(--font-size-2xs)] uppercase"
1021
+ className="text-[length:var(--font-size-2xs, calc(var(--font-size-base) / var(--font-size-ratio) / var(--font-size-ratio)))] size-7 rounded-md uppercase"
1022
1022
  title={s}
1023
1023
  >
1024
1024
  {s[0]}
@@ -23,7 +23,7 @@ import {
23
23
  AtSign,
24
24
  } from "lucide-react";
25
25
 
26
- import { Button, Text } from "@godxjp/ui/general";
26
+ import { Button, Heading, Icon, Text } from "@godxjp/ui/general";
27
27
  import { Avatar, AvatarFallback, Card, CardContent } from "@godxjp/ui/data-display";
28
28
  import { Flex } from "@godxjp/ui/layout";
29
29
 
@@ -46,6 +46,7 @@ const THEME = `
46
46
  a card, 3.21 on --muted. It was 36 18% 22% = 1.52:1. --border is decorative chrome; it stays. */
47
47
  --border: 36 22% 14%; --input: 36 18% 40%;
48
48
  --success: 145 63% 49%; --warning: 33 90% 44%; --destructive: 0 84% 60%; --info: 213 94% 68%;
49
+ --letter-spacing-tight: -0.03em; /* the display tracking .fl-display/.fl-h2 hand-wrote, once */
49
50
  --radius: 1rem; --radius-md: 10px; --radius-lg: 14px; --radius-xl: 20px; --radius-2xl: 24px;
50
51
  --card-radius: var(--radius-2xl); --control-radius: var(--radius-lg);
51
52
  --shadow-color: 0 0 0;
@@ -54,15 +55,30 @@ const THEME = `
54
55
  --font-family-display: "Sora", system-ui, sans-serif;
55
56
  --font-family-body: "Be Vietnam Pro", system-ui, "Noto Sans JP", sans-serif;
56
57
  --font-family-sans: var(--font-family-body);
57
- --font-size-display: 5rem; /* 80px hero via text-5xl */
58
+ /* 80px hero, from the display ramp's BASE KNOB alone (gh#834).
59
+ This used to need a second line — --font-size-5xl: 5rem — and the comment that stood here said
60
+ why: --font-size-5xl was declared at :root as var(--font-size-display), so it substituted on
61
+ <html> and this scope merely INHERITED the answer root had given. Heading size="5xl" painted
62
+ 54px with the display knob alone. The step is an initial knob now and the formula sits at the
63
+ call site, so one line moves the ramp: re-measured here at 80px, with 3xl/4xl following to
64
+ 48.83/62.5px. */
65
+ --font-size-display: 5rem;
66
+ /* The glass bar, opted into once (gh#831). acme-website.tsx hand-wrote the identical pair,
67
+ which is why these are framework knobs now and not two literals on .fl-navbar. */
68
+ --topbar-background-alpha: 80%;
69
+ --topbar-backdrop-blur-size: 12px;
58
70
  color-scheme: dark;
59
71
  }
60
72
  /* Marketing surface (consumer section stylesheet) */
61
73
  [data-tenant="futurelastic"] { background: hsl(var(--background)); color: hsl(var(--foreground)); min-height: 100vh; }
62
- [data-tenant="futurelastic"] .fl-shell { margin-inline: auto; width: 100%; max-width: 1140px; padding-inline: 2rem; container-type: inline-size; }
63
- [data-tenant="futurelastic"] .fl-section { padding-block: 5.5rem; }
64
- [data-tenant="futurelastic"] .fl-navbar { position: sticky; top: 0; z-index: 30; background: hsl(var(--background) / 0.8);
65
- backdrop-filter: blur(12px); border-bottom: 1px solid hsl(var(--border)); }
74
+ [data-tenant="futurelastic"] .fl-shell { margin-inline: auto; width: 100%; max-inline-size: var(--page-measure-wide); padding-inline: var(--space-8); container-type: inline-size; }
75
+ [data-tenant="futurelastic"] .fl-section { padding-block: var(--space-section-band); }
76
+ /* The sticky header IS this page's bar region, so it reads the --topbar-* glass knobs. See the
77
+ matching note in acme-website.tsx for why it is not a <Topbar>. */
78
+ [data-tenant="futurelastic"] .fl-navbar { position: sticky; inset-block-start: 0; z-index: 30;
79
+ background-color: hsl(var(--background) / var(--topbar-background-alpha));
80
+ backdrop-filter: blur(var(--topbar-backdrop-blur-size));
81
+ border-block-end: var(--stroke-hairline) solid hsl(var(--border)); }
66
82
  /* REFLOW, NOT DECORATION (gh#643). This row was a fixed 68px flex line with no wrap, so at 320 its
67
83
  content ran to x=569.6 inside a 320px nav: every link painted OUTSIDE the navbar's own
68
84
  translucent box, over the page behind it, and axe read the white labels against the light
@@ -71,53 +87,57 @@ const THEME = `
71
87
  element looks like from the outside. min-height rather than height, so a wrapped row may be
72
88
  taller than one line and the language cluster stops overflowing the bar vertically (measured at
73
89
  320: it was 148.3px tall, starting at y=-40.2). */
74
- [data-tenant="futurelastic"] .fl-navbar-inner { min-height: 68px; display: flex; align-items: center; flex-wrap: wrap; column-gap: 2rem; row-gap: 0.75rem; padding-block: 0.75rem; }
90
+ [data-tenant="futurelastic"] .fl-navbar-inner { min-block-size: 68px; display: flex; align-items: center; flex-wrap: wrap; column-gap: var(--space-8); row-gap: var(--space-3); padding-block: var(--space-3); }
75
91
  [data-tenant="futurelastic"] .fl-brand { font-family: var(--font-family-display); font-weight: 700; font-size: 1.25rem; color: hsl(var(--foreground)); }
76
92
  [data-tenant="futurelastic"] .fl-eyebrow { font-family: var(--font-family-body); font-weight: 600; font-size: 0.75rem;
77
93
  letter-spacing: 0.18em; text-transform: uppercase; color: hsl(var(--primary)); }
78
- [data-tenant="futurelastic"] .fl-badge { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.375rem 0.875rem;
79
- border: 1px solid hsl(var(--primary) / 0.3); background: hsl(var(--primary) / 0.08); border-radius: 999px;
94
+ [data-tenant="futurelastic"] .fl-badge { display: inline-flex; align-items: center; gap: var(--space-2); padding: 0.375rem 0.875rem;
95
+ border: var(--stroke-hairline) solid hsl(var(--primary) / 0.3); background: hsl(var(--primary) / 0.08); border-radius: var(--radius-pill);
80
96
  font-size: 0.8125rem; font-weight: 500; color: hsl(var(--foreground)); }
81
- [data-tenant="futurelastic"] .fl-badge .dot { width: 6px; height: 6px; border-radius: 999px; background: hsl(var(--primary)); }
82
- [data-tenant="futurelastic"] .fl-hero { position: relative; overflow: hidden; padding-block: clamp(5rem, 3rem + 9vw, 10rem); }
83
- [data-tenant="futurelastic"] .fl-hero-glow { position: absolute; inset: 0; pointer-events: none;
84
- background: radial-gradient(60% 50% at 70% 0%, hsl(42 54% 54% / 0.12), transparent 70%); }
97
+ [data-tenant="futurelastic"] .fl-badge .dot { inline-size: 6px; block-size: 6px; border-radius: var(--radius-pill); background: hsl(var(--primary)); }
98
+ /* The hero band still breathes with the viewport, but both ends of the clamp are now the band/hero
99
+ steps rather than an eyeballed 5rem…10rem (gh#831). */
100
+ [data-tenant="futurelastic"] .fl-hero { position: relative; overflow: hidden; padding-block: clamp(var(--space-section-band), 3rem + 9vw, var(--space-section-hero)); }
101
+ /* The halo is .ui-brand-glow (src/styles/layout.css), which ships and was going unused. This
102
+ class and .fl-cta-glow now only place it and set its documented knobs. */
103
+ [data-tenant="futurelastic"] .fl-hero-glow { position: absolute; inset: 0;
104
+ --brand-glow-color: 42 54% 54%; --brand-glow-size: 60% 50%; --brand-glow-position: 70% 0%; --brand-glow-alpha: 0.12; }
85
105
  [data-tenant="futurelastic"] .fl-hero-inner { position: relative; max-width: 860px; }
86
- [data-tenant="futurelastic"] .fl-display { font-family: var(--font-family-display); font-weight: 700; font-size: 5rem;
87
- line-height: 1.05; letter-spacing: -0.03em; color: hsl(var(--foreground)); margin: 1.25rem 0; }
88
- [data-tenant="futurelastic"] .fl-h2 { font-family: var(--font-family-display); font-weight: 700; font-size: 2.5rem;
89
- line-height: 1.1; letter-spacing: -0.02em; color: hsl(var(--foreground)); margin: 0.75rem 0; }
106
+ /* .fl-display / .fl-h2 / .fl-lead / .fl-hero-glow / .fl-cta-glow are GONE (gh#826) — and the fact
107
+ that this file and acme-website.tsx, two unrelated brands, had independently written the SAME
108
+ five is what made them a missing API rather than brand styling. They are now
109
+ Heading size="5xl", Heading size="4xl", Text size="xl" and two .ui-brand-glow
110
+ layers. The display face still arrives through --font-family-display (base.css points every
111
+ <h1>-<h6> at it); the tracking is a token, set once above. */
90
112
  [data-tenant="futurelastic"] .fl-gold { color: hsl(var(--primary)); }
91
- [data-tenant="futurelastic"] .fl-lead { font-size: 1.25rem; line-height: 1.65; color: hsl(var(--muted-foreground)); max-width: 620px; }
92
- [data-tenant="futurelastic"] .fl-note { font-size: 0.9375rem; color: hsl(var(--muted-foreground)); margin-top: 2.5rem; }
113
+ [data-tenant="futurelastic"] .fl-note { font-size: 0.9375rem; color: hsl(var(--muted-foreground)); margin-block-start: var(--space-10); }
93
114
  [data-tenant="futurelastic"] .fl-head { max-width: 640px; margin-inline: auto; text-align: center; }
94
- [data-tenant="futurelastic"] .fl-hero-actions { margin-top: 1.75rem; }
115
+ [data-tenant="futurelastic"] .fl-hero-actions { margin-block-start: 1.75rem; }
95
116
  [data-tenant="futurelastic"] .fl-cta-inner { position: relative; max-width: 680px; margin-inline: auto; }
96
117
  [data-tenant="futurelastic"] .fl-footer-tagline { max-width: 260px; }
97
- [data-tenant="futurelastic"] .fl-logocloud { display: flex; flex-wrap: wrap; justify-content: center; gap: 1rem 2.5rem; }
98
- [data-tenant="futurelastic"] .fl-logo-item { display: inline-flex; align-items: center; gap: 0.5rem; font-family: var(--font-family-display);
118
+ [data-tenant="futurelastic"] .fl-logocloud { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-4) var(--space-10); }
119
+ [data-tenant="futurelastic"] .fl-logo-item { display: inline-flex; align-items: center; gap: var(--space-2); font-family: var(--font-family-display);
99
120
  font-weight: 600; font-size: 1.0625rem; color: hsl(var(--foreground) / 0.85); }
100
121
  [data-tenant="futurelastic"] .fl-logo-item .tag { font-family: var(--font-family-body); font-weight: 500; font-size: 0.6875rem;
101
122
  letter-spacing: 0.06em; text-transform: uppercase; color: hsl(var(--primary)); }
102
- [data-tenant="futurelastic"] .fl-section-tint { background: hsl(var(--card) / 0.5); border-block: 1px solid hsl(var(--border)); padding-block: 3rem; }
103
- [data-tenant="futurelastic"] .fl-bento { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
123
+ [data-tenant="futurelastic"] .fl-section-tint { background: hsl(var(--card) / 0.5); border-block: var(--stroke-hairline) solid hsl(var(--border)); padding-block: var(--space-12); }
124
+ [data-tenant="futurelastic"] .fl-bento { display: grid; grid-template-columns: 1fr; gap: var(--space-5); }
104
125
  @container (min-width: 768px) { [data-tenant="futurelastic"] .fl-bento { grid-template-columns: repeat(6, 1fr); } }
105
126
  @container (min-width: 768px) { [data-tenant="futurelastic"] .fl-bento > .fl-cell { grid-column: span 2; } }
106
127
  @container (min-width: 768px) { [data-tenant="futurelastic"] .fl-bento > .fl-cell.wide { grid-column: span 4; } }
107
128
  @container (min-width: 768px) { [data-tenant="futurelastic"] .fl-bento > .fl-cell.half { grid-column: span 3; } }
108
- [data-tenant="futurelastic"] .fl-medallion svg { width: 1.5rem; height: 1.5rem; }
109
- [data-tenant="futurelastic"] .fl-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: 2rem; }
129
+ [data-tenant="futurelastic"] .fl-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-8); }
110
130
  @container (min-width: 768px) { [data-tenant="futurelastic"] .fl-stats { grid-template-columns: repeat(4, 1fr); } }
111
131
  [data-tenant="futurelastic"] .fl-stat-num { font-family: var(--font-family-display); font-weight: 700; font-size: 3rem; color: hsl(var(--primary)); line-height: 1; }
112
- [data-tenant="futurelastic"] .fl-cta { position: relative; overflow: hidden; text-align: center; border: 1px solid hsl(var(--primary) / 0.2);
113
- border-radius: var(--radius-2xl); background: hsl(var(--card)); padding: 4rem 2rem; }
114
- [data-tenant="futurelastic"] .fl-cta-glow { position: absolute; inset: 0; pointer-events: none;
115
- background: radial-gradient(60% 80% at 50% 0%, hsl(42 54% 54% / 0.14), transparent 70%); }
116
- [data-tenant="futurelastic"] .fl-footer { border-top: 1px solid hsl(var(--border)); padding-block: 3.5rem 2rem; }
117
- [data-tenant="futurelastic"] .fl-footer-grid { display: grid; grid-template-columns: 1fr; gap: 2.5rem; }
132
+ [data-tenant="futurelastic"] .fl-cta { position: relative; overflow: hidden; text-align: center; border: var(--stroke-hairline) solid hsl(var(--primary) / 0.2);
133
+ border-radius: var(--radius-2xl); background: hsl(var(--card)); padding: 4rem var(--space-8); }
134
+ [data-tenant="futurelastic"] .fl-cta-glow { position: absolute; inset: 0;
135
+ --brand-glow-color: 42 54% 54%; --brand-glow-size: 60% 80%; --brand-glow-position: 50% 0%; --brand-glow-alpha: 0.14; }
136
+ [data-tenant="futurelastic"] .fl-footer { border-block-start: var(--stroke-hairline) solid hsl(var(--border)); padding-block: 3.5rem var(--space-8); }
137
+ [data-tenant="futurelastic"] .fl-footer-grid { display: grid; grid-template-columns: 1fr; gap: var(--space-10); }
118
138
  @container (min-width: 768px) { [data-tenant="futurelastic"] .fl-footer-grid { grid-template-columns: 1.6fr 1fr 1fr 1fr; } }
119
- [data-tenant="futurelastic"] .fl-footer-bottom { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 1rem;
120
- margin-top: 2.5rem; padding-top: 1.5rem; border-top: 1px solid hsl(var(--border)); }
139
+ [data-tenant="futurelastic"] .fl-footer-bottom { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: var(--space-4);
140
+ margin-block-start: var(--space-10); padding-block-start: var(--space-6); border-block-start: var(--stroke-hairline) solid hsl(var(--border)); }
121
141
  `;
122
142
 
123
143
  const SHELL = "fl-shell";
@@ -151,18 +171,26 @@ function Navbar() {
151
171
  function Hero() {
152
172
  return (
153
173
  <header className="fl-hero">
154
- <div className="fl-hero-glow" />
174
+ <div
175
+ className="ui-brand-glow absolute inset-0"
176
+ style={
177
+ {
178
+ "--brand-glow-alpha": "0.12",
179
+ "--brand-glow-position": "70% 0%",
180
+ } as React.CSSProperties
181
+ }
182
+ />
155
183
  <div className={`${SHELL} fl-hero-inner`}>
156
184
  <span className="fl-badge">
157
185
  <span className="dot" /> Tech · AI · Holdings
158
186
  </span>
159
- <h1 className="fl-display">
187
+ <Heading level={1} size="5xl" weight="bold">
160
188
  Building the <span className="fl-gold">elastic</span> infrastructure of the future.
161
- </h1>
162
- <p className="fl-lead">
189
+ </Heading>
190
+ <Text as="p" size="xl" tone="muted">
163
191
  Futurelastic là pháp nhân holding của nhóm công ty công nghệ &amp; AI vận hành xuyên Nhật
164
192
  Bản và Việt Nam, từ logistics, fintech đến F&amp;B và thương mại.
165
- </p>
193
+ </Text>
166
194
  <Flex direction="row" gap="md" className="fl-hero-actions">
167
195
  <Button size="lg">Khám phá hệ sinh thái</Button>
168
196
  <Button variant="outline" size="lg">
@@ -201,11 +229,11 @@ function LogoCloud() {
201
229
  );
202
230
  }
203
231
 
204
- function Medallion({ icon: Icon }: { icon: typeof Boxes }) {
232
+ function Medallion({ icon: Glyph }: { icon: typeof Boxes }) {
205
233
  return (
206
234
  <Avatar className="rounded-lg">
207
- <AvatarFallback className="bg-primary/10 text-primary fl-medallion rounded-lg">
208
- <Icon aria-hidden="true" />
235
+ <AvatarFallback className="bg-primary/10 text-primary rounded-lg">
236
+ <Icon as={Glyph} size="xl" />
209
237
  </AvatarFallback>
210
238
  </Avatar>
211
239
  );
@@ -247,10 +275,12 @@ function Bento() {
247
275
  <Flex direction="col" gap="xl" className={SHELL}>
248
276
  <div className="fl-head">
249
277
  <div className="fl-eyebrow">Hệ sinh thái</div>
250
- <h2 className="fl-h2">Một mái nhà, nhiều mũi nhọn</h2>
251
- <p className="fl-lead mx-auto">
278
+ <Heading level={2} size="4xl" weight="bold">
279
+ Một mái nhà, nhiều mũi nhọn
280
+ </Heading>
281
+ <Text as="p" size="xl" tone="muted" className="mx-auto">
252
282
  Mỗi entity tự chủ vận hành; Futurelastic cung cấp vốn, công nghệ nền và quản trị chung.
253
- </p>
283
+ </Text>
254
284
  </div>
255
285
  <div className="fl-bento">
256
286
  {cells.map((c) => (
@@ -304,13 +334,23 @@ function Cta() {
304
334
  <section className="fl-section">
305
335
  <div className={SHELL}>
306
336
  <div className="fl-cta">
307
- <div className="fl-cta-glow" />
337
+ <div
338
+ className="ui-brand-glow absolute inset-0"
339
+ style={
340
+ {
341
+ "--brand-glow-alpha": "0.14",
342
+ "--brand-glow-size": "60% 80%",
343
+ } as React.CSSProperties
344
+ }
345
+ />
308
346
  <Flex direction="col" gap="xl" className="fl-cta-inner">
309
- <h2 className="fl-h2">Cùng xây điều mới?</h2>
310
- <p className="fl-lead mx-auto">
347
+ <Heading level={2} size="4xl" weight="bold">
348
+ Cùng xây điều mới?
349
+ </Heading>
350
+ <Text as="p" size="xl" tone="muted" className="mx-auto">
311
351
  Chúng tôi tìm đối tác, nhân tài và cơ hội M&amp;A trong công nghệ, logistics và
312
352
  F&amp;B tại Nhật Bản &amp; Việt Nam.
313
- </p>
353
+ </Text>
314
354
  <Flex direction="row" gap="md" className="justify-center">
315
355
  <Button size="lg">Đặt lịch trao đổi</Button>
316
356
  <Button variant="ghost" size="lg">