@hraness/design-kit 0.36.5 → 0.39.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.
- package/MARKETING_COPY.md +2 -0
- package/README.md +77 -11
- package/dist/{chunk-xa28y4fg.js → chunk-1dgm85ta.js} +191 -55
- package/dist/chunk-6mx403v5.js +286 -0
- package/dist/{chunk-thv5t11w.js → chunk-dc8v05zh.js} +6 -137
- package/dist/{chunk-evb02bc1.js → chunk-fcs5qekr.js} +48 -1
- package/dist/chunk-fxwed3e5.js +775 -0
- package/dist/chunk-vy0pt84a.js +1807 -0
- package/dist/{chunk-sabcr66p.js → chunk-w1y1m3m7.js} +300 -1
- package/dist/index.js +24 -12
- package/dist/mockups/client.js +6 -654
- package/dist/mockups/index.js +29 -1797
- package/dist/provider-marks.js +1 -1
- package/dist/react/index.js +190 -5
- package/dist/react/server.js +5 -3
- package/dist/stylex-manifest.json +1 -1
- package/package.json +9 -5
- package/portfolio-inventory.json +1 -1
- package/src/compiler-foundation.css +2 -0
- package/src/design-gallery.css +12 -0
- package/src/index.ts +1 -0
- package/src/marketing-marquee.css +281 -0
- package/src/marketing-marquee.ts +206 -0
- package/src/mockups/client.tsx +157 -66
- package/src/mockups/core.tsx +30 -0
- package/src/mockups.css +158 -23
- package/src/provider-marks.generated.ts +92 -0
- package/src/provider-marks.ts +29 -4
- package/src/react/design-gallery.tsx +53 -0
- package/src/react/index.ts +1 -0
- package/src/react/marketing-marquee.tsx +71 -0
- package/src/react/server.ts +1 -0
- package/src/styles.css +2 -0
- package/vendor/provider-marks/BOOTSTRAP-ICONS-LICENSE +21 -0
- package/vendor/provider-marks/UPSTREAM.md +28 -0
- package/vendor/provider-marks/bootstrap-icons/calendar-event-fill.svg +3 -0
- package/vendor/provider-marks/bootstrap-icons/filetype-csv.svg +3 -0
- package/vendor/provider-marks/bootstrap-icons/globe2.svg +3 -0
- package/vendor/provider-marks/bootstrap-icons/linkedin.svg +3 -0
- package/vendor/provider-marks/bootstrap-icons/microsoft.svg +3 -0
- package/vendor/provider-marks/bootstrap-icons/person-vcard-fill.svg +3 -0
- package/vendor/provider-marks/bootstrap-icons/shop.svg +3 -0
- package/vendor/provider-marks/simple-icons/apple.svg +1 -0
- package/vendor/provider-marks/simple-icons/bluesky.svg +1 -0
- package/vendor/provider-marks/simple-icons/facebook.svg +1 -0
- package/vendor/provider-marks/simple-icons/github.svg +1 -0
- package/vendor/provider-marks/simple-icons/gmail.svg +1 -0
- package/vendor/provider-marks/simple-icons/google.svg +1 -0
- package/vendor/provider-marks/simple-icons/instagram.svg +1 -0
- package/vendor/provider-marks/simple-icons/reddit.svg +1 -0
- package/vendor/provider-marks/simple-icons/substack.svg +1 -0
- package/vendor/provider-marks/simple-icons/telegram.svg +1 -0
- package/vendor/provider-marks/simple-icons/threads.svg +1 -0
- package/vendor/provider-marks/simple-icons/tiktok.svg +1 -0
- package/vendor/provider-marks/simple-icons/twitch.svg +1 -0
- package/vendor/provider-marks/simple-icons/x.svg +1 -0
- package/vendor/provider-marks/simple-icons/ycombinator.svg +1 -0
- package/vendor/provider-marks/simple-icons/youtube.svg +1 -0
- package/dist/chunk-gmea6p0d.js +0 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/design-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
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",
|
|
@@ -128,7 +128,8 @@
|
|
|
128
128
|
"import": "./dist/testing.js"
|
|
129
129
|
},
|
|
130
130
|
"./marketing-comparison.css": "./src/marketing-comparison.css",
|
|
131
|
-
"./marketing-diagram.css": "./src/marketing-diagram.css"
|
|
131
|
+
"./marketing-diagram.css": "./src/marketing-diagram.css",
|
|
132
|
+
"./marketing-marquee.css": "./src/marketing-marquee.css"
|
|
132
133
|
},
|
|
133
134
|
"main": "./dist/index.js",
|
|
134
135
|
"types": "./src/index.ts",
|
|
@@ -310,7 +311,10 @@
|
|
|
310
311
|
"src/diagrams.ts",
|
|
311
312
|
"src/react/marketing-diagram.tsx",
|
|
312
313
|
"src/react/marketing-diagram.stylex.ts",
|
|
313
|
-
"src/marketing-diagram.css"
|
|
314
|
+
"src/marketing-diagram.css",
|
|
315
|
+
"src/marketing-marquee.ts",
|
|
316
|
+
"src/marketing-marquee.css",
|
|
317
|
+
"src/react/marketing-marquee.tsx"
|
|
314
318
|
],
|
|
315
319
|
"scripts": {
|
|
316
320
|
"build": "bun run ./scripts/build.ts",
|
|
@@ -323,8 +327,8 @@
|
|
|
323
327
|
"test": "bun test --preload ./scripts/register-stylex-test-transform.ts ./src",
|
|
324
328
|
"browser:install": "bun ./node_modules/playwright-core/cli.js install chromium",
|
|
325
329
|
"test:browser": "bun run test:browser:marketing && bun run test:browser:rest",
|
|
326
|
-
"test:browser:marketing": "bun run ./scripts/product-marketing-browser.ts
|
|
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",
|
|
330
|
+
"test:browser:marketing": "bun run ./scripts/product-marketing-browser.ts",
|
|
331
|
+
"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/marketing-marquee-browser.ts && bun run ./scripts/platform-install-browser.ts && bun run ./scripts/mockups-browser.ts && bun run ./scripts/agent-setup-browser.ts",
|
|
328
332
|
"test:package": "bun run ./scripts/package-smoke.ts && bun run ./scripts/mockups-import.ts",
|
|
329
333
|
"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",
|
|
330
334
|
"kb:check:lane": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.24.0/hraness-wordcell-0.24.0.tgz wordcell check --root kb --no-catalog",
|
package/portfolio-inventory.json
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
@import "./plain-publication.css";
|
|
14
14
|
@import "./reading.css";
|
|
15
15
|
@import "./status-page.css";
|
|
16
|
+
@import "./marketing-marquee.css";
|
|
16
17
|
@import "./product-marketing-foundation.css";
|
|
17
18
|
@import "./design-gallery.css";
|
|
19
|
+
@import "./mockups.css";
|
|
18
20
|
@import "./lantern-material.css";
|
package/src/design-gallery.css
CHANGED
|
@@ -7,6 +7,18 @@
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
@layer components.hraness-design-kit.legacy {
|
|
10
|
+
.design-gallery__walkthrough-preview { display: grid; gap: 1rem; padding: 1.5rem; font-size: 0.875rem; }
|
|
11
|
+
.design-gallery__walkthrough-context { margin: 0; color: var(--hkm-muted); font-size: 0.75rem; letter-spacing: 0.06em; }
|
|
12
|
+
.design-gallery__walkthrough-title { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin: 0; font-size: 1.125rem; font-weight: 600; }
|
|
13
|
+
.design-gallery__walkthrough-state { padding: 0.25rem 0.5rem; border: 1px solid var(--hkm-line); border-radius: 0.375rem; color: var(--hkm-muted); font-size: 0.75rem; font-weight: 500; }
|
|
14
|
+
.design-gallery__walkthrough-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; margin: 0; }
|
|
15
|
+
.design-gallery__walkthrough-facts dt { margin-block-end: 0.25rem; color: var(--hkm-muted); font-size: 0.75rem; }
|
|
16
|
+
.design-gallery__walkthrough-facts dd { margin: 0; }
|
|
17
|
+
.design-gallery__walkthrough-checks { margin: 0; padding: 0; list-style: none; }
|
|
18
|
+
.design-gallery__walkthrough-checks li { display: flex; justify-content: space-between; gap: 1rem; padding-block: 0.75rem; border-block-start: 1px solid var(--hkm-line); }
|
|
19
|
+
.design-gallery__walkthrough-checks li > :last-child { color: var(--hkm-muted); }
|
|
20
|
+
.design-gallery__walkthrough-result { display: grid; gap: 0.375rem; padding: 1rem; border: 1px solid var(--hkm-line); border-radius: 0.625rem; background: var(--hkm-surface); }
|
|
21
|
+
.design-gallery__walkthrough-result > span { color: var(--hkm-muted); font-size: 0.75rem; }
|
|
10
22
|
.design-gallery__lantern-intro {
|
|
11
23
|
margin: 0;
|
|
12
24
|
max-inline-size: 60ch;
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./article-html.js";
|
|
|
8
8
|
export * from "./status-page.js";
|
|
9
9
|
export * from "./status-page-html.js";
|
|
10
10
|
export * from "./provider-marks.js";
|
|
11
|
+
export * from "./marketing-marquee.js";
|
|
11
12
|
export * from "./platforms.js";
|
|
12
13
|
export * from "./relative-time.js";
|
|
13
14
|
export * from "./agent-setup.js";
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/* Raw HTML counterpart to MarketingMarquee and renderMarketingMarqueeHtml.
|
|
2
|
+
* The static layout is the default: a wrapped list with hidden duplicates.
|
|
3
|
+
* Motion is added only on screens whose readers allow it, so reduced motion,
|
|
4
|
+
* print, and unsupported media all keep every name readable in place. */
|
|
5
|
+
@layer components.hraness-design-kit.legacy {
|
|
6
|
+
.hraness-marketing-marquee {
|
|
7
|
+
--_hraness-marquee-ink: var(--hraness-marketing-ink, var(--foreground, CanvasText));
|
|
8
|
+
--_hraness-marquee-muted: var(--hraness-marketing-muted, var(--muted, GrayText));
|
|
9
|
+
--_hraness-marquee-line: var(--hraness-marketing-line, color-mix(in srgb, var(--_hraness-marquee-ink) 12%, transparent));
|
|
10
|
+
--_hraness-marquee-line-strong: var(--hraness-marketing-line-strong, color-mix(in srgb, var(--_hraness-marquee-ink) 22%, transparent));
|
|
11
|
+
--_hraness-marquee-gap: clamp(1.75rem, 3vw, 2.5rem);
|
|
12
|
+
--_hraness-marquee-fade: clamp(1.5rem, 6vw, 4.5rem);
|
|
13
|
+
position: relative;
|
|
14
|
+
display: grid;
|
|
15
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
16
|
+
grid-template-areas: "header header" "viewport control";
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: 1rem 0.75rem;
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
inline-size: min(100%, var(--hraness-marketing-measure, 72rem));
|
|
21
|
+
min-inline-size: 0;
|
|
22
|
+
margin-inline: auto;
|
|
23
|
+
padding-block: clamp(1.5rem, 3vw, 2.25rem);
|
|
24
|
+
padding-inline: var(--hraness-marketing-gutter, clamp(1.25rem, 4vw, 3rem));
|
|
25
|
+
color: var(--_hraness-marquee-muted);
|
|
26
|
+
font-family: var(--hraness-marketing-text-font, var(--font-text, inherit));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.hraness-marketing-marquee__header {
|
|
30
|
+
grid-area: header;
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-wrap: wrap;
|
|
33
|
+
align-items: baseline;
|
|
34
|
+
gap: 0.25rem 1rem;
|
|
35
|
+
min-inline-size: 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.hraness-marketing-marquee[data-align="center"] .hraness-marketing-marquee__header {
|
|
39
|
+
justify-content: center;
|
|
40
|
+
text-align: center;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.hraness-marketing-marquee__label {
|
|
44
|
+
margin: 0;
|
|
45
|
+
color: var(--_hraness-marquee-ink);
|
|
46
|
+
font-size: 1rem;
|
|
47
|
+
font-weight: 500;
|
|
48
|
+
line-height: 1.5;
|
|
49
|
+
text-wrap: balance;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.hraness-marketing-marquee__count {
|
|
53
|
+
font-weight: 650;
|
|
54
|
+
font-variant-numeric: tabular-nums;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.hraness-marketing-marquee__action {
|
|
58
|
+
display: inline-flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
min-block-size: 1.5rem;
|
|
61
|
+
color: var(--_hraness-marquee-muted);
|
|
62
|
+
font-size: 0.875rem;
|
|
63
|
+
line-height: 1.5;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.hraness-marketing-marquee__action:hover {
|
|
67
|
+
color: var(--_hraness-marquee-ink);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.hraness-marketing-marquee__viewport {
|
|
71
|
+
grid-area: viewport;
|
|
72
|
+
min-inline-size: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.hraness-marketing-marquee__list {
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-wrap: wrap;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 0.75rem var(--_hraness-marquee-gap);
|
|
80
|
+
margin: 0;
|
|
81
|
+
padding: 0;
|
|
82
|
+
list-style: none;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.hraness-marketing-marquee[data-align="center"] .hraness-marketing-marquee__list {
|
|
86
|
+
justify-content: center;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.hraness-marketing-marquee__list[aria-hidden="true"] {
|
|
90
|
+
display: none;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.hraness-marketing-marquee__item {
|
|
94
|
+
display: inline-flex;
|
|
95
|
+
flex: none;
|
|
96
|
+
align-items: center;
|
|
97
|
+
gap: 0.5rem;
|
|
98
|
+
color: var(--_hraness-marquee-muted);
|
|
99
|
+
font-size: 0.9375rem;
|
|
100
|
+
font-weight: 500;
|
|
101
|
+
line-height: 1.5;
|
|
102
|
+
white-space: nowrap;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.hraness-marketing-marquee__mark {
|
|
106
|
+
flex: none;
|
|
107
|
+
inline-size: 1.25rem;
|
|
108
|
+
block-size: 1.25rem;
|
|
109
|
+
fill: currentColor;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.hraness-marketing-marquee__monogram {
|
|
113
|
+
display: inline-grid;
|
|
114
|
+
flex: none;
|
|
115
|
+
place-items: center;
|
|
116
|
+
box-sizing: border-box;
|
|
117
|
+
inline-size: 1.25rem;
|
|
118
|
+
block-size: 1.25rem;
|
|
119
|
+
border: 1.5px solid currentColor;
|
|
120
|
+
border-radius: 0.3125rem;
|
|
121
|
+
font-size: 0.5625rem;
|
|
122
|
+
font-weight: 700;
|
|
123
|
+
letter-spacing: 0.02em;
|
|
124
|
+
line-height: 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.hraness-marketing-marquee__sprite {
|
|
128
|
+
position: absolute;
|
|
129
|
+
inline-size: 0;
|
|
130
|
+
block-size: 0;
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.hraness-marketing-marquee__toggle,
|
|
135
|
+
.hraness-marketing-marquee__control {
|
|
136
|
+
display: none;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.hraness-marketing-marquee__control-text {
|
|
140
|
+
position: absolute;
|
|
141
|
+
inline-size: 1px;
|
|
142
|
+
block-size: 1px;
|
|
143
|
+
overflow: hidden;
|
|
144
|
+
clip-path: inset(50%);
|
|
145
|
+
white-space: nowrap;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.hraness-marketing-marquee__control-icon {
|
|
149
|
+
inline-size: 1rem;
|
|
150
|
+
block-size: 1rem;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.hraness-marketing-marquee__control-icon[data-icon="play"],
|
|
154
|
+
.hraness-marketing-marquee__toggle:checked ~ .hraness-marketing-marquee__control .hraness-marketing-marquee__control-icon[data-icon="pause"] {
|
|
155
|
+
display: none;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.hraness-marketing-marquee__toggle:checked ~ .hraness-marketing-marquee__control .hraness-marketing-marquee__control-icon[data-icon="play"] {
|
|
159
|
+
display: block;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* About 3.75 seconds per item, so long and short lists move at a similar pace. */
|
|
163
|
+
.hraness-marketing-marquee__list:has(> :nth-child(8)) { --_hraness-marquee-duration: 30s; }
|
|
164
|
+
.hraness-marketing-marquee__list:has(> :nth-child(10)) { --_hraness-marquee-duration: 38s; }
|
|
165
|
+
.hraness-marketing-marquee__list:has(> :nth-child(12)) { --_hraness-marquee-duration: 45s; }
|
|
166
|
+
.hraness-marketing-marquee__list:has(> :nth-child(14)) { --_hraness-marquee-duration: 53s; }
|
|
167
|
+
.hraness-marketing-marquee__list:has(> :nth-child(16)) { --_hraness-marquee-duration: 60s; }
|
|
168
|
+
.hraness-marketing-marquee__list:has(> :nth-child(18)) { --_hraness-marquee-duration: 68s; }
|
|
169
|
+
.hraness-marketing-marquee__list:has(> :nth-child(20)) { --_hraness-marquee-duration: 75s; }
|
|
170
|
+
.hraness-marketing-marquee__list:has(> :nth-child(24)) { --_hraness-marquee-duration: 90s; }
|
|
171
|
+
.hraness-marketing-marquee__list:has(> :nth-child(28)) { --_hraness-marquee-duration: 105s; }
|
|
172
|
+
.hraness-marketing-marquee__list:has(> :nth-child(32)) { --_hraness-marquee-duration: 120s; }
|
|
173
|
+
.hraness-marketing-marquee__list:has(> :nth-child(40)) { --_hraness-marquee-duration: 150s; }
|
|
174
|
+
.hraness-marketing-marquee__list:has(> :nth-child(48)) { --_hraness-marquee-duration: 180s; }
|
|
175
|
+
.hraness-marketing-marquee__list:has(> :nth-child(56)) { --_hraness-marquee-duration: 210s; }
|
|
176
|
+
|
|
177
|
+
@media screen and (prefers-reduced-motion: no-preference) {
|
|
178
|
+
.hraness-marketing-marquee__viewport {
|
|
179
|
+
overflow: hidden;
|
|
180
|
+
mask-image: linear-gradient(to right, transparent, #000 var(--_hraness-marquee-fade), #000 calc(100% - var(--_hraness-marquee-fade)), transparent);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.hraness-marketing-marquee__track {
|
|
184
|
+
display: flex;
|
|
185
|
+
inline-size: max-content;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.hraness-marketing-marquee__list,
|
|
189
|
+
.hraness-marketing-marquee__list[aria-hidden="true"] {
|
|
190
|
+
display: flex;
|
|
191
|
+
flex: none;
|
|
192
|
+
flex-wrap: nowrap;
|
|
193
|
+
padding-inline-end: var(--_hraness-marquee-gap);
|
|
194
|
+
animation: hraness-marketing-marquee var(--hraness-marketing-marquee-duration, var(--_hraness-marquee-duration, 26s)) linear infinite;
|
|
195
|
+
will-change: transform;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.hraness-marketing-marquee[data-align="center"] .hraness-marketing-marquee__list {
|
|
199
|
+
justify-content: flex-start;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
:is([dir="rtl"] .hraness-marketing-marquee__list, .hraness-marketing-marquee__list:dir(rtl)) {
|
|
203
|
+
animation-name: hraness-marketing-marquee-rtl;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.hraness-marketing-marquee__toggle {
|
|
207
|
+
display: block;
|
|
208
|
+
position: absolute;
|
|
209
|
+
inline-size: 1px;
|
|
210
|
+
block-size: 1px;
|
|
211
|
+
margin: 0;
|
|
212
|
+
padding: 0;
|
|
213
|
+
border: 0;
|
|
214
|
+
overflow: hidden;
|
|
215
|
+
clip-path: inset(50%);
|
|
216
|
+
white-space: nowrap;
|
|
217
|
+
appearance: none;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.hraness-marketing-marquee__control {
|
|
221
|
+
grid-area: control;
|
|
222
|
+
display: inline-flex;
|
|
223
|
+
align-items: center;
|
|
224
|
+
justify-content: center;
|
|
225
|
+
box-sizing: border-box;
|
|
226
|
+
inline-size: 2.75rem;
|
|
227
|
+
block-size: 2.75rem;
|
|
228
|
+
border: 1px solid var(--_hraness-marquee-line);
|
|
229
|
+
border-radius: 999px;
|
|
230
|
+
color: var(--_hraness-marquee-muted);
|
|
231
|
+
cursor: pointer;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.hraness-marketing-marquee__control:hover {
|
|
235
|
+
border-color: var(--_hraness-marquee-line-strong);
|
|
236
|
+
color: var(--_hraness-marquee-ink);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.hraness-marketing-marquee__toggle:focus-visible ~ .hraness-marketing-marquee__control {
|
|
240
|
+
outline: 2px solid var(--hraness-marketing-accent, var(--focus, Highlight));
|
|
241
|
+
outline-offset: 2px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.hraness-marketing-marquee__toggle:checked ~ .hraness-marketing-marquee__viewport .hraness-marketing-marquee__list {
|
|
245
|
+
animation-play-state: paused;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@media screen and (prefers-reduced-motion: no-preference) and (hover: hover) {
|
|
250
|
+
.hraness-marketing-marquee__viewport:hover .hraness-marketing-marquee__list {
|
|
251
|
+
animation-play-state: paused;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
@media (pointer: coarse) {
|
|
256
|
+
.hraness-marketing-marquee__action {
|
|
257
|
+
min-block-size: 2.75rem;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
@media (forced-colors: active) {
|
|
262
|
+
.hraness-marketing-marquee__viewport {
|
|
263
|
+
mask-image: none;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.hraness-marketing-marquee__control {
|
|
267
|
+
border-color: ButtonBorder;
|
|
268
|
+
color: ButtonText;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@keyframes hraness-marketing-marquee {
|
|
273
|
+
from { transform: translateX(0); }
|
|
274
|
+
to { transform: translateX(-100%); }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@keyframes hraness-marketing-marquee-rtl {
|
|
278
|
+
from { transform: translateX(0); }
|
|
279
|
+
to { transform: translateX(100%); }
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-neutral provider marquee: a quiet, slowly scrolling band of
|
|
3
|
+
* provider marks and names under a label that states how many there are.
|
|
4
|
+
* `resolveMarketingMarquee` validates the input once; the static renderer here
|
|
5
|
+
* and `MarketingMarquee` in `@hraness/design-kit/react/server` emit identical
|
|
6
|
+
* markup from that model, styled by `marketing-marquee.css` (included in
|
|
7
|
+
* `styles.css`).
|
|
8
|
+
*
|
|
9
|
+
* The band moves only with `prefers-reduced-motion: no-preference` on screen.
|
|
10
|
+
* Otherwise, and in print, it is a wrapped static list. A native checkbox
|
|
11
|
+
* pauses the motion without JavaScript, hovering pauses it, and duplicate
|
|
12
|
+
* copies that fill the loop are hidden from assistive technology.
|
|
13
|
+
*/
|
|
14
|
+
import { assertArticleHref } from "./article.js";
|
|
15
|
+
import { escapeArticleHtml as escape } from "./article-html.js";
|
|
16
|
+
import { providerMark, providerMarkMonogram, type ProviderMarkDescriptor } from "./provider-marks.js";
|
|
17
|
+
|
|
18
|
+
/** The placeholder a label must contain once; it renders as the item count. */
|
|
19
|
+
export const MARKETING_MARQUEE_COUNT_TOKEN = "{count}";
|
|
20
|
+
export const MARKETING_MARQUEE_MAX_ITEMS = 64;
|
|
21
|
+
export const MARKETING_MARQUEE_DEFAULT_PAUSE_LABEL = "Pause scrolling";
|
|
22
|
+
|
|
23
|
+
/** Each loop renders at least this many items after its first copy, so wide viewports never show a gap. */
|
|
24
|
+
const MIN_LOOP_ITEMS = 20;
|
|
25
|
+
const MAX_NAME_LENGTH = 64;
|
|
26
|
+
const MAX_LABEL_LENGTH = 120;
|
|
27
|
+
const ID_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/u;
|
|
28
|
+
const FORBIDDEN_ARTWORK = /<script|<foreignObject|on[a-z]+\s*=|javascript:/iu;
|
|
29
|
+
const ROOT_CLASS = "hraness-marketing-marquee";
|
|
30
|
+
|
|
31
|
+
export type MarketingMarqueeAlign = "start" | "center";
|
|
32
|
+
|
|
33
|
+
export interface MarketingMarqueeItem {
|
|
34
|
+
/** The visible name, which is also the item's accessible text. */
|
|
35
|
+
readonly name: string;
|
|
36
|
+
/**
|
|
37
|
+
* A registered provider mark id, display name, or alias, or a descriptor
|
|
38
|
+
* from the provider registry. Defaults to `name`. Identities outside the
|
|
39
|
+
* registry show a two-letter monogram instead of invented artwork.
|
|
40
|
+
*/
|
|
41
|
+
readonly mark?: ProviderMarkDescriptor | string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface MarketingMarqueeAction {
|
|
45
|
+
readonly href: string;
|
|
46
|
+
readonly label: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface MarketingMarqueeInput {
|
|
50
|
+
/** A unique element id; the label, pause control, and mark symbols derive their ids from it. */
|
|
51
|
+
readonly id: string;
|
|
52
|
+
/** One sentence containing `{count}` exactly once, such as "Works with {count} services". */
|
|
53
|
+
readonly label: string;
|
|
54
|
+
readonly items: readonly MarketingMarqueeItem[];
|
|
55
|
+
/** An optional link beside the label, such as the full provider list. */
|
|
56
|
+
readonly action?: MarketingMarqueeAction;
|
|
57
|
+
readonly align?: MarketingMarqueeAlign;
|
|
58
|
+
readonly className?: string;
|
|
59
|
+
/** The pause checkbox's accessible name. */
|
|
60
|
+
readonly pauseLabel?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface MarketingMarqueeSymbol {
|
|
64
|
+
readonly id: string;
|
|
65
|
+
readonly viewBox: string;
|
|
66
|
+
readonly body: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type MarketingMarqueeMark =
|
|
70
|
+
| Readonly<{ kind: "glyph"; symbolId: string }>
|
|
71
|
+
| Readonly<{ kind: "monogram"; monogram: string }>;
|
|
72
|
+
|
|
73
|
+
export interface ResolvedMarketingMarqueeItem {
|
|
74
|
+
readonly name: string;
|
|
75
|
+
readonly mark: MarketingMarqueeMark;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ResolvedMarketingMarquee {
|
|
79
|
+
readonly id: string;
|
|
80
|
+
readonly labelId: string;
|
|
81
|
+
readonly toggleId: string;
|
|
82
|
+
readonly className: string;
|
|
83
|
+
readonly align: MarketingMarqueeAlign;
|
|
84
|
+
/** The label split around the rendered count. */
|
|
85
|
+
readonly label: Readonly<{ before: string; count: string; after: string }>;
|
|
86
|
+
readonly items: readonly ResolvedMarketingMarqueeItem[];
|
|
87
|
+
/** One symbol per distinct glyph, referenced by every copy. */
|
|
88
|
+
readonly symbols: readonly MarketingMarqueeSymbol[];
|
|
89
|
+
/** Total list copies: the accessible list plus hidden duplicates that fill the loop. */
|
|
90
|
+
readonly copies: number;
|
|
91
|
+
readonly action: MarketingMarqueeAction | null;
|
|
92
|
+
readonly pauseLabel: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** SVG paths for the pause control, drawn in a 16-unit square. */
|
|
96
|
+
export const MARKETING_MARQUEE_CONTROL_ICONS = {
|
|
97
|
+
pause: "M5 3.25a1 1 0 0 1 1 1v7.5a1 1 0 0 1-2 0v-7.5a1 1 0 0 1 1-1Zm6 0a1 1 0 0 1 1 1v7.5a1 1 0 0 1-2 0v-7.5a1 1 0 0 1 1-1Z",
|
|
98
|
+
play: "M5.25 3.6v8.8a.75.75 0 0 0 1.14.64l7.04-4.4a.75.75 0 0 0 0-1.28L6.39 2.96a.75.75 0 0 0-1.14.64Z",
|
|
99
|
+
} as const;
|
|
100
|
+
|
|
101
|
+
function requireText(value: string, field: string, limit: number): string {
|
|
102
|
+
const text = value.trim();
|
|
103
|
+
if (text === "") throw new RangeError(`Marketing marquee ${field} must not be empty.`);
|
|
104
|
+
if (text.length > limit) throw new RangeError(`Marketing marquee ${field} must be at most ${limit} characters.`);
|
|
105
|
+
return text;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveMark(mark: ProviderMarkDescriptor | string | undefined, name: string): ProviderMarkDescriptor | undefined {
|
|
109
|
+
if (mark === undefined) return providerMark(name);
|
|
110
|
+
if (typeof mark === "string") return providerMark(mark);
|
|
111
|
+
return mark;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Validate marquee input and compute everything both renderers need. */
|
|
115
|
+
export function resolveMarketingMarquee(input: MarketingMarqueeInput): ResolvedMarketingMarquee {
|
|
116
|
+
if (!ID_PATTERN.test(input.id)) throw new RangeError("Marketing marquee id must start with a letter and use at most 64 letters, digits, hyphens, or underscores.");
|
|
117
|
+
const label = requireText(input.label, "label", MAX_LABEL_LENGTH);
|
|
118
|
+
const parts = label.split(MARKETING_MARQUEE_COUNT_TOKEN);
|
|
119
|
+
if (parts.length !== 2) throw new RangeError(`Marketing marquee label must contain ${MARKETING_MARQUEE_COUNT_TOKEN} exactly once.`);
|
|
120
|
+
if (input.items.length === 0) throw new RangeError("Marketing marquee needs at least one item.");
|
|
121
|
+
if (input.items.length > MARKETING_MARQUEE_MAX_ITEMS) throw new RangeError(`Marketing marquee shows at most ${MARKETING_MARQUEE_MAX_ITEMS} items.`);
|
|
122
|
+
if (input.align !== undefined && input.align !== "start" && input.align !== "center") throw new RangeError("Marketing marquee align must be start or center.");
|
|
123
|
+
|
|
124
|
+
const names = new Set<string>();
|
|
125
|
+
const symbols: MarketingMarqueeSymbol[] = [];
|
|
126
|
+
const symbolIds = new Map<string, string>();
|
|
127
|
+
const items = input.items.map((item): ResolvedMarketingMarqueeItem => {
|
|
128
|
+
const name = requireText(item.name, "item name", MAX_NAME_LENGTH);
|
|
129
|
+
if (names.has(name)) throw new RangeError(`Marketing marquee lists ${JSON.stringify(name)} more than once.`);
|
|
130
|
+
names.add(name);
|
|
131
|
+
const descriptor = resolveMark(item.mark, name);
|
|
132
|
+
if (descriptor === undefined || descriptor.glyph.body === "") {
|
|
133
|
+
return { name, mark: { kind: "monogram", monogram: descriptor?.monogram ?? providerMarkMonogram(name) } };
|
|
134
|
+
}
|
|
135
|
+
const { viewBox, body } = descriptor.glyph;
|
|
136
|
+
if (FORBIDDEN_ARTWORK.test(body) || /["<>]/u.test(viewBox)) throw new RangeError(`Marketing marquee artwork for ${JSON.stringify(name)} contains unsupported markup.`);
|
|
137
|
+
const key = `${viewBox}\n${body}`;
|
|
138
|
+
let symbolId = symbolIds.get(key);
|
|
139
|
+
if (symbolId === undefined) {
|
|
140
|
+
symbolId = `${input.id}-mark-${symbols.length}`;
|
|
141
|
+
symbolIds.set(key, symbolId);
|
|
142
|
+
symbols.push({ body, id: symbolId, viewBox });
|
|
143
|
+
}
|
|
144
|
+
return { name, mark: { kind: "glyph", symbolId } };
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
let action: MarketingMarqueeAction | null = null;
|
|
148
|
+
if (input.action !== undefined) {
|
|
149
|
+
assertArticleHref(input.action.href);
|
|
150
|
+
action = { href: input.action.href, label: requireText(input.action.label, "action label", MAX_LABEL_LENGTH) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
action,
|
|
155
|
+
align: input.align ?? "start",
|
|
156
|
+
className: [ROOT_CLASS, input.className?.trim()].filter(Boolean).join(" "),
|
|
157
|
+
copies: Math.max(2, 1 + Math.ceil(MIN_LOOP_ITEMS / items.length)),
|
|
158
|
+
id: input.id,
|
|
159
|
+
items,
|
|
160
|
+
label: { after: parts[1] ?? "", before: parts[0] ?? "", count: String(items.length) },
|
|
161
|
+
labelId: `${input.id}-label`,
|
|
162
|
+
pauseLabel: requireText(input.pauseLabel ?? MARKETING_MARQUEE_DEFAULT_PAUSE_LABEL, "pause label", MAX_LABEL_LENGTH),
|
|
163
|
+
symbols,
|
|
164
|
+
toggleId: `${input.id}-pause`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function itemHtml(item: ResolvedMarketingMarqueeItem): string {
|
|
169
|
+
const mark = item.mark.kind === "glyph"
|
|
170
|
+
? `<svg aria-hidden="true" class="${ROOT_CLASS}__mark" fill="currentColor" focusable="false" height="20" width="20"><use href="#${item.mark.symbolId}"></use></svg>`
|
|
171
|
+
: `<span aria-hidden="true" class="${ROOT_CLASS}__monogram">${escape(item.mark.monogram)}</span>`;
|
|
172
|
+
return `<li class="${ROOT_CLASS}__item">${mark}<span class="${ROOT_CLASS}__name">${escape(item.name)}</span></li>`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function controlIconHtml(icon: keyof typeof MARKETING_MARQUEE_CONTROL_ICONS): string {
|
|
176
|
+
return `<svg aria-hidden="true" class="${ROOT_CLASS}__control-icon" data-icon="${icon}" fill="currentColor" focusable="false" height="16" viewBox="0 0 16 16" width="16"><path d="${MARKETING_MARQUEE_CONTROL_ICONS[icon]}"></path></svg>`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Static HTML for a provider marquee. Pair it with `marketing-marquee.css` or `styles.css`. */
|
|
180
|
+
export function renderMarketingMarqueeHtml(input: MarketingMarqueeInput): string {
|
|
181
|
+
const marquee = resolveMarketingMarquee(input);
|
|
182
|
+
const items = marquee.items.map(itemHtml).join("");
|
|
183
|
+
const lists = Array.from({ length: marquee.copies }, (_, index) => (
|
|
184
|
+
`<ul${index === 0 ? "" : ' aria-hidden="true"'} class="${ROOT_CLASS}__list">${items}</ul>`
|
|
185
|
+
)).join("");
|
|
186
|
+
const sprite = marquee.symbols.length === 0
|
|
187
|
+
? ""
|
|
188
|
+
: `<svg aria-hidden="true" class="${ROOT_CLASS}__sprite" focusable="false" height="0" width="0">${marquee.symbols.map((symbol) => (
|
|
189
|
+
`<symbol id="${symbol.id}" viewBox="${symbol.viewBox}">${symbol.body}</symbol>`
|
|
190
|
+
)).join("")}</svg>`;
|
|
191
|
+
const action = marquee.action === null
|
|
192
|
+
? ""
|
|
193
|
+
: `<a class="${ROOT_CLASS}__action hraness-text-link" href="${escape(marquee.action.href)}">${escape(marquee.action.label)}</a>`;
|
|
194
|
+
return [
|
|
195
|
+
`<section aria-labelledby="${marquee.labelId}" class="${escape(marquee.className)}" data-align="${marquee.align}" data-hraness-marketing="marquee" id="${marquee.id}">`,
|
|
196
|
+
sprite,
|
|
197
|
+
`<div class="${ROOT_CLASS}__header">`,
|
|
198
|
+
`<p class="${ROOT_CLASS}__label" id="${marquee.labelId}">${escape(marquee.label.before)}<strong class="${ROOT_CLASS}__count">${marquee.label.count}</strong>${escape(marquee.label.after)}</p>`,
|
|
199
|
+
action,
|
|
200
|
+
"</div>",
|
|
201
|
+
`<input class="${ROOT_CLASS}__toggle" id="${marquee.toggleId}" type="checkbox"/>`,
|
|
202
|
+
`<div class="${ROOT_CLASS}__viewport"><div class="${ROOT_CLASS}__track">${lists}</div></div>`,
|
|
203
|
+
`<label class="${ROOT_CLASS}__control" for="${marquee.toggleId}"><span class="${ROOT_CLASS}__control-text">${escape(marquee.pauseLabel)}</span>${controlIconHtml("pause")}${controlIconHtml("play")}</label>`,
|
|
204
|
+
"</section>",
|
|
205
|
+
].join("");
|
|
206
|
+
}
|