@hraness/design-kit 0.36.4 → 0.36.5

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.
package/dist/stylex.css CHANGED
@@ -3066,10 +3066,6 @@
3066
3066
  min-block-size: 2.75rem;
3067
3067
  }
3068
3068
 
3069
- .xd0akbl {
3070
- min-block-size: 2rem;
3071
- }
3072
-
3073
3069
  .xo67i2s {
3074
3070
  min-block-size: 3.25rem;
3075
3071
  }
@@ -3090,12 +3086,16 @@
3090
3086
  min-block-size: var(--hraness-marketing-header-action-height, 2rem);
3091
3087
  }
3092
3088
 
3093
- .x1jqubh4 {
3094
- min-block-size: var(--hraness-marketing-header-height, 2.75rem);
3089
+ .x5c9nq0 {
3090
+ min-block-size: var(--hraness-marketing-header-control-target, 2rem);
3091
+ }
3092
+
3093
+ .x5zky9q {
3094
+ min-block-size: var(--hraness-marketing-header-control-target, var(--interactive-target-compact, 2.75rem));
3095
3095
  }
3096
3096
 
3097
- .x1qwoi4t {
3098
- min-block-size: var(--interactive-target-compact, 2.75rem);
3097
+ .x1jqubh4 {
3098
+ min-block-size: var(--hraness-marketing-header-height, 2.75rem);
3099
3099
  }
3100
3100
 
3101
3101
  .x1j3ipif {
@@ -3114,10 +3114,6 @@
3114
3114
  min-inline-size: 0;
3115
3115
  }
3116
3116
 
3117
- .x1o8ym9z {
3118
- min-inline-size: 2rem;
3119
- }
3120
-
3121
3117
  .xze7o54 {
3122
3118
  min-inline-size: 40rem;
3123
3119
  }
@@ -3134,8 +3130,12 @@
3134
3130
  min-inline-size: 9rem;
3135
3131
  }
3136
3132
 
3137
- .x9hh0qe {
3138
- min-inline-size: var(--interactive-target-compact, 2.75rem);
3133
+ .xye2cm2 {
3134
+ min-inline-size: var(--hraness-marketing-header-control-target, 2rem);
3135
+ }
3136
+
3137
+ .xq5ipov {
3138
+ min-inline-size: var(--hraness-marketing-header-control-target, var(--interactive-target-compact, 2.75rem));
3139
3139
  }
3140
3140
 
3141
3141
  .xz1t8dp {
@@ -4202,15 +4202,23 @@
4202
4202
  .xn27wch.xn27wch {
4203
4203
  min-block-size: max(2.75rem,var(--hraness-marketing-header-action-height, 2rem));
4204
4204
  }
4205
+ }
4206
+
4207
+ @media (pointer: coarse) {
4208
+ .xczs0xf.xczs0xf {
4209
+ min-block-size: max(3rem,var(--hraness-marketing-header-control-target, 3rem));
4210
+ }
4211
+ }
4205
4212
 
4213
+ @media (width <= 48rem) {
4206
4214
  .x1l2grcz.x1l2grcz {
4207
4215
  min-inline-size: 0;
4208
4216
  }
4209
4217
  }
4210
4218
 
4211
4219
  @media (pointer: coarse) {
4212
- .x11je3w3.x11je3w3 {
4213
- min-inline-size: 3rem;
4220
+ .x12c0uzw.x12c0uzw {
4221
+ min-inline-size: max(3rem,var(--hraness-marketing-header-control-target, 3rem));
4214
4222
  }
4215
4223
  }
4216
4224
 
@@ -5699,6 +5707,10 @@
5699
5707
  --hraness-marketing-grid-columns: initial;
5700
5708
  }
5701
5709
 
5710
+ .xlhppvv {
5711
+ --hraness-marketing-header-control-target: 2.75rem;
5712
+ }
5713
+
5702
5714
  .xphms39 {
5703
5715
  --hraness-marketing-pillar-columns: 1;
5704
5716
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/design-kit",
3
- "version": "0.36.4",
3
+ "version": "0.36.5",
4
4
  "description": "React components and CSS for Hraness apps and product sites: application shells, marketing sections, charts, themes, effects, and syntax highlighting. Built on @hraness/ui.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -143,6 +143,7 @@
143
143
  "src/browser/artifact-share.ts",
144
144
  "src/browser/foil.ts",
145
145
  "src/browser/index.ts",
146
+ "src/browser/marketing-header.ts",
146
147
  "src/browser/status-field.ts",
147
148
  "src/browser/status-page.ts",
148
149
  "src/browser/sticky-offset.ts",
@@ -322,7 +323,7 @@
322
323
  "test": "bun test --preload ./scripts/register-stylex-test-transform.ts ./src",
323
324
  "browser:install": "bun ./node_modules/playwright-core/cli.js install chromium",
324
325
  "test:browser": "bun run test:browser:marketing && bun run test:browser:rest",
325
- "test:browser:marketing": "bun run ./scripts/product-marketing-browser.ts",
326
+ "test:browser:marketing": "bun run ./scripts/product-marketing-browser.ts && bun run ./scripts/marketing-header-browser.ts",
326
327
  "test:browser:rest": "bun run ./scripts/site-shell-browser.ts && bun run ./scripts/palette-browser.ts && bun run ./scripts/gallery-browser.ts && bun run ./scripts/global-error-browser.ts && bun run ./scripts/security-delivery-browser.ts && bun run ./scripts/paper-theme-browser.ts && bun run ./scripts/product-marketing-preset-browser.ts && bun run ./scripts/lantern-material-browser.ts && bun run ./scripts/foil-browser.ts && bun run ./scripts/hero-light-browser.ts && bun run ./scripts/status-page-browser.ts && bun run ./scripts/platform-install-browser.ts && bun run ./scripts/mockups-browser.ts && bun run ./scripts/agent-setup-browser.ts",
327
328
  "test:package": "bun run ./scripts/package-smoke.ts && bun run ./scripts/mockups-import.ts",
328
329
  "kb:refresh": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.24.0/hraness-wordcell-0.24.0.tgz wordcell refresh --root kb",
@@ -6,7 +6,7 @@
6
6
  "name": "@hraness/design-kit",
7
7
  "path": ".",
8
8
  "visibility": "public",
9
- "version": "0.36.4"
9
+ "version": "0.36.5"
10
10
  }
11
11
  ],
12
12
  "contract": "hraness.portfolio-inventory/v1",
@@ -5,6 +5,7 @@ export * from "./artifact-share.js";
5
5
  export * from "./design-palette.js";
6
6
  export * from "./foil.js";
7
7
  export * from "./hero-light.js";
8
+ export * from "./marketing-header.js";
8
9
  export * from "./status-page.js";
9
10
  export * from "./sticky-offset.js";
10
11
  export * from "../palette-appearance.js";
@@ -0,0 +1,113 @@
1
+ export interface MarketingHeaderOptions {
2
+ readonly brandHref?: string;
3
+ readonly brandLabel?: string;
4
+ readonly brandMark?: string;
5
+ /** Fixed-theme and authentication pages may explicitly omit appearance. */
6
+ readonly appearance?: "required" | "omitted";
7
+ /** Turn off geometry only when inspecting an unrendered DOM in unit tests. */
8
+ readonly measure?: boolean;
9
+ /** Require hydrated sticky clearance to match the live header height. */
10
+ readonly stickyOffset?: boolean;
11
+ }
12
+
13
+ export interface MarketingHeaderInspection {
14
+ readonly headerCount: number;
15
+ readonly appearanceCount: number;
16
+ readonly brandHref: string | null;
17
+ readonly brandLabel: string | null;
18
+ readonly brandMark: string | null;
19
+ readonly links: readonly Readonly<{ href: string; label: string }>[];
20
+ readonly problems: readonly string[];
21
+ }
22
+
23
+ /**
24
+ * Inspect a complete public page, including article, documentation, and status
25
+ * routes. This read-only function is self-contained so browser runners can use
26
+ * `page.evaluate(inspectMarketingHeader, options)` without loading a test script.
27
+ */
28
+ export function inspectMarketingHeader(
29
+ options: MarketingHeaderOptions = {},
30
+ document: Document = globalThis.document,
31
+ ): MarketingHeaderInspection {
32
+ const problems: string[] = [];
33
+ const headers = document.querySelectorAll('header[data-hraness-marketing="header"], header.hraness-marketing-header, header.hraness-marketing-header-surface');
34
+ const appearances = document.querySelectorAll("[data-hraness-appearance-menu]");
35
+ const mains = document.querySelectorAll("main");
36
+ const header = headers[0];
37
+ if (headers.length !== 1) problems.push(`Expected one product header; found ${headers.length}.`);
38
+ if (mains.length !== 1) problems.push(`Expected one main landmark; found ${mains.length}.`);
39
+ const appearanceCount = options.appearance === "omitted" ? 0 : 1;
40
+ if (appearances.length !== appearanceCount) problems.push(`Expected ${appearanceCount} appearance controls; found ${appearances.length}.`);
41
+
42
+ const brand = header?.querySelector<HTMLAnchorElement>("a.hraness-marketing-header__brand");
43
+ const mark = brand?.querySelector(".hraness-foil-mark");
44
+ const image = mark?.querySelector("img");
45
+ const brandHref = brand?.getAttribute("href") ?? null;
46
+ const brandLabel = (brand?.getAttribute("aria-label") ?? brand?.textContent)?.trim() || null;
47
+ const brandMark = image?.getAttribute("src") ?? null;
48
+ const nav = header?.querySelector("nav.hraness-marketing-header__nav");
49
+ const links = [...(nav?.querySelectorAll("a[href]") ?? [])].map(link => ({
50
+ href: link.getAttribute("href") ?? "",
51
+ label: (link.getAttribute("aria-label") ?? link.textContent)?.trim() ?? "",
52
+ }));
53
+ if (header !== undefined) {
54
+ const main = mains[0];
55
+ if (main !== undefined && (main.contains(header) || header.contains(main) || !(header.compareDocumentPosition(main) & 4))) {
56
+ problems.push("The product header must precede and sit outside the main landmark.");
57
+ }
58
+ if (brandHref !== (options.brandHref ?? "/")) problems.push("The product home link is missing or points to a different page.");
59
+ if (brandLabel === null || (options.brandLabel !== undefined && brandLabel !== options.brandLabel)) problems.push("The product home link has a missing or inconsistent name.");
60
+ if (brand?.hasAttribute("data-foil") !== true || mark === null || mark === undefined || image === null || image === undefined || !mark.querySelector(".hraness-foil-mark__paint") || !brandMark) {
61
+ problems.push("The product home link is missing its foil mark and artwork fallback.");
62
+ } else if (options.brandMark !== undefined && brandMark !== options.brandMark) {
63
+ problems.push("The product header uses different mark artwork.");
64
+ }
65
+ if (!nav || !(nav.getAttribute("aria-label")?.trim() || nav.getAttribute("aria-labelledby")?.trim()) || links.length === 0 || links.some(link => !link.label)) {
66
+ problems.push("The product header needs a named navigation with readable links.");
67
+ }
68
+ const appearance = appearances[0];
69
+ const actions = header.querySelector(".hraness-marketing-header__actions");
70
+ if (appearanceCount === 1 && appearance !== undefined) {
71
+ if (!actions?.contains(appearance) || !actions.lastElementChild?.contains(appearance)) {
72
+ problems.push("Appearance must be the final product-header action.");
73
+ }
74
+ if (!appearance.querySelector(":scope > summary, :scope > button, button.hraness-design-theme-toggle__trigger")) problems.push("Appearance is missing its trigger.");
75
+ }
76
+
77
+ if (options.measure !== false) {
78
+ const view = document.defaultView;
79
+ if (view === null) problems.push("Rendered header inspection requires a browser window.");
80
+ else {
81
+ const trigger = appearance?.querySelector(":scope > summary, :scope > button, button.hraness-design-theme-toggle__trigger");
82
+ for (const [name, element] of [["header", header], ["brand", brand], ["mark", mark], ["navigation", nav], ["appearance", appearanceCount === 1 ? appearance : null], ["appearance trigger", appearanceCount === 1 ? trigger : null]] as const) {
83
+ if (!element) continue;
84
+ const box = element.getBoundingClientRect();
85
+ let hidden = false;
86
+ for (let ancestor: Element | null = element; ancestor !== null; ancestor = ancestor.parentElement) {
87
+ const style = view.getComputedStyle(ancestor);
88
+ if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse" || Number(style.opacity) === 0) hidden = true;
89
+ }
90
+ if (box.width <= 0 || box.height <= 0 || hidden) problems.push(`The product ${name} is not visible.`);
91
+ }
92
+ const box = header.getBoundingClientRect();
93
+ if (box.left < -1 || box.right > view.innerWidth + 1) problems.push("The product header extends outside the viewport.");
94
+ const inner = header.querySelector(".hraness-marketing-header__inner");
95
+ if (!inner || !["grid", "flex"].includes(view.getComputedStyle(inner).display)) problems.push("The shared product-header layout styles are missing.");
96
+ if (options.stickyOffset === true && view.getComputedStyle(header).position === "sticky") {
97
+ for (const target of [document.documentElement, mains[0]]) {
98
+ if (!target) continue;
99
+ const offset = Number.parseFloat(view.getComputedStyle(target).getPropertyValue("--hraness-sticky-offset"));
100
+ if (!Number.isFinite(offset) || Math.abs(offset - box.height) > 1) problems.push("Sticky clearance does not match the measured product header.");
101
+ }
102
+ }
103
+ if (appearanceCount === 1 && trigger) {
104
+ const triggerBox = trigger.getBoundingClientRect();
105
+ if (triggerBox.width < 43.5 || triggerBox.height < 43.5) problems.push("The appearance trigger is smaller than 44px.");
106
+ const actionLinks = actions?.querySelectorAll("a[href]") ?? [];
107
+ if ([...actionLinks].some(link => link.getBoundingClientRect().right > triggerBox.right + 1)) problems.push("Appearance is not the rightmost header action.");
108
+ }
109
+ }
110
+ }
111
+ }
112
+ return { headerCount: headers.length, appearanceCount: appearances.length, brandHref, brandLabel, brandMark, links, problems };
113
+ }
@@ -32,10 +32,13 @@ function resolveHeader(
32
32
  return root.querySelector(stickyOffsetHeaderSelector);
33
33
  }
34
34
 
35
- function resolveTarget(header: Element, target: HTMLElement | undefined): HTMLElement {
36
- if (target !== undefined) return target;
37
- const page = header.closest<HTMLElement>(".hraness-marketing-page");
38
- return page ?? header.ownerDocument.documentElement;
35
+ function resolveTargets(header: Element, target: HTMLElement | undefined): readonly HTMLElement[] {
36
+ if (target !== undefined) return [target];
37
+ const targets = new Set<HTMLElement>([header.ownerDocument.documentElement]);
38
+ for (let ancestor: Element | null = header; ancestor !== null; ancestor = ancestor.parentElement) {
39
+ if (ancestor.matches(".hraness-marketing-page, [data-hraness-marketing-preset]")) targets.add(ancestor as HTMLElement);
40
+ }
41
+ return [...targets];
39
42
  }
40
43
 
41
44
  /** Border-box height of sticky marketing chrome, as a CSS length. */
@@ -43,10 +46,10 @@ export function measureStickyOffset(header: Element): string {
43
46
  return `${header.getBoundingClientRect().height}px`;
44
47
  }
45
48
 
46
- /** Write --hraness-sticky-offset onto the page or document so siblings inherit it. */
49
+ /** Publish on the document and enclosing page/preset scopes so local defaults cannot mask the measurement. */
47
50
  export function publishStickyOffset(header: Element, target?: HTMLElement): string {
48
51
  const value = measureStickyOffset(header);
49
- resolveTarget(header, target).style.setProperty(stickyOffsetCustomProperty, value);
52
+ for (const destination of resolveTargets(header, target)) destination.style.setProperty(stickyOffsetCustomProperty, value);
50
53
  return value;
51
54
  }
52
55
 
@@ -62,21 +65,30 @@ export function syncStickyOffset(options: StickyOffsetSyncOptions = {}): () => v
62
65
  const root = options.root ?? document;
63
66
  const header = resolveHeader(root, options.header);
64
67
  if (header === null) return () => {};
65
- const target = resolveTarget(header, options.target);
68
+ const targets = resolveTargets(header, options.target);
69
+ const prior = targets.map(target => ({ target, value: target.style.getPropertyValue(stickyOffsetCustomProperty), priority: typeof target.style.getPropertyPriority === "function" ? target.style.getPropertyPriority(stickyOffsetCustomProperty) : "" }));
70
+ let published: string;
66
71
  const publish = () => {
67
- publishStickyOffset(header, target);
72
+ published = measureStickyOffset(header);
73
+ for (const target of targets) target.style.setProperty(stickyOffsetCustomProperty, published);
74
+ };
75
+ const restore = () => {
76
+ for (const { target, value, priority } of prior) {
77
+ if (target.style.getPropertyValue(stickyOffsetCustomProperty) !== published
78
+ || (typeof target.style.getPropertyPriority === "function" && target.style.getPropertyPriority(stickyOffsetCustomProperty) !== "")) continue;
79
+ if (value) target.style.setProperty(stickyOffsetCustomProperty, value, priority);
80
+ else target.style.removeProperty(stickyOffsetCustomProperty);
81
+ }
68
82
  };
69
83
  publish();
70
84
  const view = document.defaultView;
71
85
  if (view === null || typeof view.ResizeObserver !== "function") {
72
- return () => {
73
- target.style.removeProperty(stickyOffsetCustomProperty);
74
- };
86
+ return restore;
75
87
  }
76
88
  const observer = new view.ResizeObserver(publish);
77
89
  observer.observe(header);
78
90
  return () => {
79
91
  observer.disconnect();
80
- target.style.removeProperty(stickyOffsetCustomProperty);
92
+ restore();
81
93
  };
82
94
  }
@@ -38,8 +38,8 @@ export const paletteMenuStyles = stylex.create({
38
38
  display: "inline-flex",
39
39
  justifyContent: "center",
40
40
  listStyleType: "none",
41
- "min-block-size": "var(--interactive-target-compact, 2.75rem)",
42
- "min-inline-size": "var(--interactive-target-compact, 2.75rem)",
41
+ "min-block-size": "var(--hraness-marketing-header-control-target, var(--interactive-target-compact, 2.75rem))",
42
+ "min-inline-size": "var(--hraness-marketing-header-control-target, var(--interactive-target-compact, 2.75rem))",
43
43
  outlineColor: "var(--hraness-material-focus, var(--focus, Highlight))",
44
44
  outlineOffset: 3,
45
45
  paddingBlock: "0.5rem",
@@ -88,6 +88,7 @@ export const marketingStyles = stylex.create({
88
88
  "-webkit-font-smoothing": "antialiased"
89
89
  },
90
90
  "header": {
91
+ "--hraness-marketing-header-control-target": "2.75rem",
91
92
  "--hraness-sticky-offset": "calc(var(--hraness-marketing-header-height, 2.75rem) + 1px)",
92
93
  "color": {
93
94
  "default": "var(--hraness-marketing-ink)",
@@ -226,12 +226,12 @@ export const themeStyles = stylex.create({
226
226
  },
227
227
  lineHeight: "inherit",
228
228
  "min-block-size": {
229
- default: "2rem",
230
- [coarsePointer]: "3rem",
229
+ default: "var(--hraness-marketing-header-control-target, 2rem)",
230
+ [coarsePointer]: "max(3rem, var(--hraness-marketing-header-control-target, 3rem))",
231
231
  },
232
232
  "min-inline-size": {
233
- default: "2rem",
234
- [coarsePointer]: "3rem",
233
+ default: "var(--hraness-marketing-header-control-target, 2rem)",
234
+ [coarsePointer]: "max(3rem, var(--hraness-marketing-header-control-target, 3rem))",
235
235
  },
236
236
  opacity: {
237
237
  default: null,