@ciderpress/ui 1.0.0-rc.10 → 1.0.0-rc.11

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 (80) hide show
  1. package/dist/head/css/loader-apple.css +1 -1
  2. package/dist/head/css/loader-dots.css +1 -1
  3. package/dist/head/css/themes/amber.css +1 -1
  4. package/dist/head/css/themes/arcade.css +1 -1
  5. package/dist/head/css/themes/grannysmith.css +1 -1
  6. package/dist/head/css/themes/honeycrisp.css +1 -1
  7. package/dist/head/css/themes/midnight.css +1 -1
  8. package/dist/head/css/themes/mulled.css +1 -1
  9. package/dist/node.mjs +408 -15
  10. package/dist/theme/components/home/cta.css +15 -0
  11. package/dist/theme/components/home/cta.tsx +10 -2
  12. package/dist/theme/components/home/feature-card.css +1 -1
  13. package/dist/theme/components/home/feature-card.tsx +3 -2
  14. package/dist/theme/components/home/feature.tsx +34 -31
  15. package/dist/theme/components/home/hero-demo.css +11 -0
  16. package/dist/theme/components/home/home-visual.tsx +226 -0
  17. package/dist/theme/components/home/layout.tsx +350 -171
  18. package/dist/theme/components/home/split.css +46 -19
  19. package/dist/theme/components/home/split.tsx +28 -8
  20. package/dist/theme/components/home/tabs.css +299 -0
  21. package/dist/theme/components/home/tabs.tsx +351 -0
  22. package/dist/theme/components/home/trust-strip.css +41 -0
  23. package/dist/theme/components/home/trust-strip.tsx +168 -11
  24. package/dist/theme/components/home/workspaces.tsx +171 -69
  25. package/dist/theme/components/workspaces/card.css +4 -0
  26. package/dist/theme/components/workspaces/card.tsx +3 -3
  27. package/dist/theme/components/workspaces/grid.tsx +18 -6
  28. package/dist/theme/index.tsx +5 -0
  29. package/dist/theme/lib/rich-text-parse.ts +740 -0
  30. package/dist/theme/lib/rich-text.test.tsx +342 -0
  31. package/dist/theme/lib/rich-text.tsx +86 -0
  32. package/dist/theme/styles/overrides/fonts.css +7 -4
  33. package/dist/theme/styles/overrides/home.css +11 -2
  34. package/dist/theme/styles/overrides/rspress.css +10 -5
  35. package/dist/theme/styles/overrides/tokens.css +41 -23
  36. package/dist/theme/styles/rich-text.css +106 -0
  37. package/dist/theme/styles/themes/amber.css +32 -2
  38. package/dist/theme/styles/themes/arcade.css +16 -1
  39. package/dist/theme/styles/themes/grannysmith.css +32 -2
  40. package/dist/theme/styles/themes/honeycrisp.css +37 -7
  41. package/dist/theme/styles/themes/midnight.css +16 -1
  42. package/dist/theme/styles/themes/mulled.css +54 -9
  43. package/package.json +3 -3
  44. package/src/theme/components/home/cta.css +15 -0
  45. package/src/theme/components/home/cta.tsx +10 -2
  46. package/src/theme/components/home/feature-card.css +1 -1
  47. package/src/theme/components/home/feature-card.tsx +3 -2
  48. package/src/theme/components/home/feature.tsx +34 -31
  49. package/src/theme/components/home/hero-demo.css +11 -0
  50. package/src/theme/components/home/home-visual.tsx +226 -0
  51. package/src/theme/components/home/layout.tsx +350 -171
  52. package/src/theme/components/home/split.css +46 -19
  53. package/src/theme/components/home/split.tsx +28 -8
  54. package/src/theme/components/home/tabs.css +299 -0
  55. package/src/theme/components/home/tabs.tsx +351 -0
  56. package/src/theme/components/home/trust-strip.css +41 -0
  57. package/src/theme/components/home/trust-strip.tsx +168 -11
  58. package/src/theme/components/home/workspaces.tsx +171 -69
  59. package/src/theme/components/workspaces/card.css +4 -0
  60. package/src/theme/components/workspaces/card.tsx +3 -3
  61. package/src/theme/components/workspaces/grid.tsx +18 -6
  62. package/src/theme/index.tsx +5 -0
  63. package/src/theme/lib/rich-text-parse.ts +740 -0
  64. package/src/theme/lib/rich-text.test.tsx +342 -0
  65. package/src/theme/lib/rich-text.tsx +86 -0
  66. package/src/theme/styles/overrides/fonts.css +7 -4
  67. package/src/theme/styles/overrides/home.css +11 -2
  68. package/src/theme/styles/overrides/rspress.css +10 -5
  69. package/src/theme/styles/overrides/tokens.css +41 -23
  70. package/src/theme/styles/rich-text.css +106 -0
  71. package/src/theme/styles/themes/amber.css +32 -2
  72. package/src/theme/styles/themes/arcade.css +16 -1
  73. package/src/theme/styles/themes/grannysmith.css +32 -2
  74. package/src/theme/styles/themes/honeycrisp.css +37 -7
  75. package/src/theme/styles/themes/midnight.css +16 -1
  76. package/src/theme/styles/themes/mulled.css +54 -9
  77. package/dist/theme/components/home/hero-demo-custom.tsx +0 -116
  78. package/dist/theme/components/home/split-visual-custom.tsx +0 -26
  79. package/src/theme/components/home/hero-demo-custom.tsx +0 -116
  80. package/src/theme/components/home/split-visual-custom.tsx +0 -26
@@ -24,6 +24,24 @@
24
24
  }
25
25
  }
26
26
 
27
+ /* Copy-only band — no visual to sit beside, so the copy owns the row. */
28
+ .cp-split__inner--full {
29
+ grid-template-columns: 1fr;
30
+ }
31
+
32
+ /* Reverse layout — visual first, copy second. Scoped to the two-column
33
+ grid: the narrow single-column stack always leads with the copy, so a
34
+ reversed band doesn't push its image above the headline. */
35
+ @media (min-width: 881px) {
36
+ .cp-split__inner--reverse .cp-split__copy {
37
+ order: 2;
38
+ }
39
+
40
+ .cp-split__inner--reverse .cp-split__visual {
41
+ order: 1;
42
+ }
43
+ }
44
+
27
45
  .cp-split__copy {
28
46
  min-width: 0;
29
47
  }
@@ -33,10 +51,16 @@
33
51
  font-size: var(--cp-fs-eyebrow);
34
52
  text-transform: uppercase;
35
53
  letter-spacing: var(--cp-letter-eyebrow);
36
- color: var(--cp-c-brand-1);
54
+ /* Same shade pair as `.cp-accent` — `--cp-c-brand-1` is a fill colour and
55
+ misses 4.5:1 at this size on several themes. See styles/rich-text.css. */
56
+ color: var(--cp-c-brand-2, var(--cp-c-brand-1));
37
57
  margin-bottom: var(--cp-s-12);
38
58
  }
39
59
 
60
+ html[data-cp-variant='dark'] .cp-split__eyebrow {
61
+ color: var(--cp-c-brand-light, var(--cp-c-brand-1));
62
+ }
63
+
40
64
  .cp-split__title {
41
65
  font-size: var(--cp-fs-split-title);
42
66
  font-weight: 600;
@@ -115,7 +139,7 @@
115
139
  border-color: var(--cp-c-brand-2);
116
140
  }
117
141
 
118
- /* Code visual styling — works for any pre/code or window child */
142
+ /* Visual frame — wraps a native code block, an image, or a window child */
119
143
  .cp-split__visual {
120
144
  border: 1px solid var(--cp-c-border);
121
145
  border-radius: var(--cp-radius-lg);
@@ -123,26 +147,29 @@
123
147
  background: var(--cp-code-block-bg);
124
148
  }
125
149
 
126
- .cp-split__visual pre {
150
+ /* Native Rspress code block (Shiki) fills the frame — strip its own
151
+ margin and rounded corners so the frame owns the chrome. */
152
+ .cp-split__visual .rp-codeblock {
127
153
  margin: 0;
128
- padding: var(--cp-s-18) var(--cp-s-20);
129
- font-family: var(--cp-font-family-mono);
130
- font-size: var(--cp-fs-code);
131
- line-height: var(--cp-lh-code);
132
- white-space: pre;
133
- overflow-x: auto;
134
- color: var(--cp-c-text-1);
154
+ border-radius: 0;
135
155
  }
136
156
 
137
- .cp-split__visual .tok-kw {
138
- color: var(--cp-c-syntax-kw);
139
- }
140
- .cp-split__visual .tok-str {
141
- color: var(--cp-c-syntax-str);
157
+ .cp-split__visual .rp-codeblock pre {
158
+ overflow-x: auto;
142
159
  }
143
- .cp-split__visual .tok-fn {
144
- color: var(--cp-c-syntax-fn);
160
+
161
+ /* Terminal visual reuses the hero demo chrome; the split frame already
162
+ owns the border, radius, and shadow, so flatten the inner shell. */
163
+ .cp-split__visual .cp-hero-demo {
164
+ max-width: none;
165
+ border: 0;
166
+ border-radius: 0;
167
+ box-shadow: none;
145
168
  }
146
- .cp-split__visual .tok-cmt {
147
- color: var(--cp-c-text-3);
169
+
170
+ /* Image visual fills the frame width, preserving aspect ratio. */
171
+ .cp-split__img {
172
+ display: block;
173
+ width: 100%;
174
+ height: auto;
148
175
  }
@@ -1,6 +1,7 @@
1
- import { match } from 'massaman/match'
1
+ import { match, P } from 'massaman/match'
2
2
  import type React from 'react'
3
3
 
4
+ import { renderRichText } from '../../lib/rich-text.tsx'
4
5
  import { RouteLink } from '../../lib/route-link.tsx'
5
6
  import { safeUrl } from '../../lib/safe-url.ts'
6
7
 
@@ -43,9 +44,16 @@ export interface SplitProps {
43
44
  */
44
45
  readonly action?: SplitAction
45
46
  /**
46
- * Right-side content — typically a code preview or screenshot.
47
+ * Visual content — typically a code preview or screenshot. Omit (or
48
+ * pass `null`) for a copy-only band; the visual column collapses
49
+ * instead of painting an empty frame.
47
50
  */
48
- readonly visual: React.ReactNode
51
+ readonly visual?: React.ReactNode
52
+ /**
53
+ * Flip the column order — visual first, copy second. Defaults to
54
+ * `false` (copy first, visual second).
55
+ */
56
+ readonly reverse?: boolean
49
57
  }
50
58
 
51
59
  /**
@@ -56,17 +64,25 @@ export interface SplitProps {
56
64
  * @returns React element.
57
65
  */
58
66
  export function HomeSplit(props: SplitProps): React.ReactElement {
59
- const { eyebrow, title, body, bullets, action, visual } = props
67
+ const { eyebrow, title, body, bullets, action, visual, reverse } = props
60
68
  const list = bullets ?? []
69
+ // Without a visual the band is a single column — the two-column grid
70
+ // would otherwise leave half the row empty.
71
+ const layoutClass = match(visual)
72
+ .with(P.nullish, () => 'cp-split__inner cp-split__inner--full')
73
+ .otherwise(() => 'cp-split__inner')
74
+ const innerClass = match(reverse ?? false)
75
+ .with(true, () => `${layoutClass} cp-split__inner--reverse`)
76
+ .otherwise(() => layoutClass)
61
77
 
62
78
  return (
63
79
  <section className="cp-split">
64
- <div className="cp-split__inner">
80
+ <div className={innerClass}>
65
81
  <div className="cp-split__copy">
66
82
  {match(eyebrow)
67
83
  .with(undefined, () => null)
68
84
  .otherwise((e) => (
69
- <div className="cp-split__eyebrow">{e}</div>
85
+ <div className="cp-split__eyebrow">{renderRichText(e)}</div>
70
86
  ))}
71
87
  <h2 className="cp-split__title">{title}</h2>
72
88
  {match(body)
@@ -81,7 +97,7 @@ export function HomeSplit(props: SplitProps): React.ReactElement {
81
97
  {list.map((bullet) => (
82
98
  <li key={bullet}>
83
99
  <span className="cp-split__check">✓</span>
84
- <span>{bullet}</span>
100
+ <span>{renderRichText(bullet)}</span>
85
101
  </li>
86
102
  ))}
87
103
  </ul>
@@ -100,7 +116,11 @@ export function HomeSplit(props: SplitProps): React.ReactElement {
100
116
  )
101
117
  })}
102
118
  </div>
103
- <div className="cp-split__visual">{visual}</div>
119
+ {match(visual)
120
+ .with(P.nullish, () => null)
121
+ .otherwise((v) => (
122
+ <div className="cp-split__visual">{v}</div>
123
+ ))}
104
124
  </div>
105
125
  </section>
106
126
  )
@@ -0,0 +1,299 @@
1
+ .cp-tabs {
2
+ padding: var(--cp-s-96) var(--cp-s-32);
3
+ }
4
+
5
+ @media (max-width: 720px) {
6
+ .cp-tabs {
7
+ padding: var(--cp-s-64) var(--cp-s-20);
8
+ }
9
+ }
10
+
11
+ .cp-tabs__inner {
12
+ max-width: var(--cp-size-split-max);
13
+ margin: 0 auto;
14
+ }
15
+
16
+ /* ── Vertical band — strip beside the panel ──────────────────── */
17
+ .cp-tabs--vertical .cp-tabs__inner {
18
+ display: grid;
19
+ grid-template-columns: minmax(220px, 320px) minmax(0, 1fr);
20
+ gap: var(--cp-s-48);
21
+ align-items: start;
22
+ }
23
+
24
+ /* Reverse — panel left, strip right. Scoped to the two-column grid so
25
+ the narrow stack always leads with the tabs.
26
+
27
+ The TRACKS swap, not the items. `order` changes which track each item
28
+ lands in but leaves the track sizes where they are, so ordering alone
29
+ put the panel — headline, body, bullets and its visual — into the
30
+ 220-320px strip column and stretched the tab labels across the 1fr
31
+ remainder. The idiom was borrowed from split.css, where the columns are
32
+ a symmetric `1fr 1fr` and reordering is equivalent. */
33
+ @media (min-width: 881px) {
34
+ .cp-tabs--vertical.cp-tabs--reverse .cp-tabs__inner {
35
+ grid-template-columns: minmax(0, 1fr) minmax(220px, 320px);
36
+ }
37
+
38
+ .cp-tabs--vertical.cp-tabs--reverse .cp-tabs__list {
39
+ order: 2;
40
+ }
41
+
42
+ .cp-tabs--vertical.cp-tabs--reverse .cp-tabs__panel {
43
+ order: 1;
44
+ }
45
+ }
46
+
47
+ @media (max-width: 880px) {
48
+ .cp-tabs--vertical .cp-tabs__inner {
49
+ grid-template-columns: 1fr;
50
+ gap: var(--cp-s-32);
51
+ }
52
+ }
53
+
54
+ .cp-tabs--vertical .cp-tabs__list {
55
+ display: flex;
56
+ flex-direction: column;
57
+ gap: var(--cp-s-4);
58
+ }
59
+
60
+ .cp-tabs--vertical .cp-tabs__tab[data-selected] {
61
+ box-shadow: inset 2px 0 0 var(--cp-c-brand-1);
62
+ }
63
+
64
+ .cp-tabs--vertical .cp-tabs__panel {
65
+ display: flex;
66
+ flex-direction: column;
67
+ gap: var(--cp-s-24);
68
+ min-width: 0;
69
+ }
70
+
71
+ /* ── Horizontal band — strip above the panel ─────────────────── */
72
+ .cp-tabs--horizontal .cp-tabs__inner {
73
+ display: flex;
74
+ flex-direction: column;
75
+ gap: var(--cp-s-40);
76
+ }
77
+
78
+ .cp-tabs--horizontal .cp-tabs__list {
79
+ display: flex;
80
+ flex-wrap: wrap;
81
+ gap: var(--cp-s-8);
82
+ border-bottom: 1px solid var(--cp-c-border);
83
+ }
84
+
85
+ .cp-tabs--horizontal .cp-tabs__tab {
86
+ border-radius: var(--cp-radius-md) var(--cp-radius-md) 0 0;
87
+ }
88
+
89
+ .cp-tabs--horizontal .cp-tabs__tab[data-selected] {
90
+ box-shadow: inset 0 -2px 0 var(--cp-c-brand-1);
91
+ }
92
+
93
+ .cp-tabs--horizontal .cp-tabs__panel {
94
+ display: grid;
95
+ grid-template-columns: 1fr 1fr;
96
+ gap: var(--cp-s-48);
97
+ align-items: center;
98
+ }
99
+
100
+ @media (max-width: 880px) {
101
+ .cp-tabs--horizontal .cp-tabs__panel {
102
+ grid-template-columns: 1fr;
103
+ gap: var(--cp-s-32);
104
+ }
105
+ }
106
+
107
+ /* Copy-only tab — no visual to sit beside, so the copy owns the row. */
108
+ .cp-tabs--horizontal .cp-tabs__panel--full {
109
+ grid-template-columns: 1fr;
110
+ }
111
+
112
+ /* ── Tab ─────────────────────────────────────────────────────── */
113
+ .cp-tabs__tab {
114
+ display: flex;
115
+ align-items: center;
116
+ gap: var(--cp-s-12);
117
+ padding: var(--cp-s-12) var(--cp-s-16);
118
+ border-radius: var(--cp-radius-md);
119
+ font-size: var(--cp-fs-body);
120
+ color: var(--cp-c-text-2);
121
+ cursor: pointer;
122
+ transition:
123
+ background var(--cp-duration-fast) var(--cp-ease-base),
124
+ color var(--cp-duration-fast) var(--cp-ease-base);
125
+ }
126
+
127
+ .cp-tabs__tab[data-hovered] {
128
+ background: var(--cp-c-bg-soft);
129
+ color: var(--cp-c-text-1);
130
+ }
131
+
132
+ .cp-tabs__tab[data-selected] {
133
+ background: var(--cp-c-brand-soft);
134
+ color: var(--cp-c-text-1);
135
+ font-weight: 600;
136
+ }
137
+
138
+ .cp-tabs__tab[data-focus-visible] {
139
+ outline: var(--cp-size-focus-ring) solid var(--cp-c-brand-1);
140
+ outline-offset: var(--cp-size-focus-ring-offset);
141
+ }
142
+
143
+ /* Shrink the shared card icon chip to strip scale. */
144
+ .cp-tabs__tab .cp-card__icon {
145
+ width: var(--cp-size-icon-box-sm);
146
+ height: var(--cp-size-icon-box-sm);
147
+ }
148
+
149
+ .cp-tabs__tab .cp-card__icon svg {
150
+ width: var(--cp-size-icon-svg-sm);
151
+ height: var(--cp-size-icon-svg-sm);
152
+ }
153
+
154
+ .cp-tabs__tab-label {
155
+ min-width: 0;
156
+ }
157
+
158
+ /* ── Panel copy ──────────────────────────────────────────────── */
159
+ .cp-tabs__copy {
160
+ min-width: 0;
161
+ }
162
+
163
+ .cp-tabs__title {
164
+ font-size: clamp(20px, 2.2vw, 26px);
165
+ font-weight: 600;
166
+ letter-spacing: var(--cp-letter-display);
167
+ line-height: var(--cp-lh-display);
168
+ margin: 0 0 var(--cp-s-12);
169
+ color: var(--cp-c-text-1);
170
+ }
171
+
172
+ .cp-tabs__body {
173
+ font-size: var(--cp-fs-body);
174
+ line-height: var(--cp-lh-relaxed);
175
+ color: var(--cp-c-text-2);
176
+ margin: 0 0 var(--cp-s-20);
177
+ }
178
+
179
+ .cp-tabs__bullets {
180
+ list-style: none;
181
+ margin: 0 0 var(--cp-s-20);
182
+ padding: 0;
183
+ display: flex;
184
+ flex-direction: column;
185
+ gap: var(--cp-s-12);
186
+ }
187
+
188
+ .cp-tabs__bullets li {
189
+ display: flex;
190
+ gap: var(--cp-s-12);
191
+ font-size: var(--cp-fs-bullet);
192
+ color: var(--cp-c-text-1);
193
+ }
194
+
195
+ /* The glyph is real text (`✓`), so contrast rules apply to it. Against the
196
+ near-transparent `--cp-c-brand-soft` chip it is effectively brand-on-page,
197
+ which measured 2.27:1 on mulled/dark and under 4:1 on four other combos —
198
+ legible as a smudge at best. `--cp-c-text-1` on the same chip is the
199
+ pairing already used by `.cp-mark`, and clears 12.7:1 everywhere. */
200
+ .cp-tabs__check {
201
+ width: var(--cp-size-check);
202
+ height: var(--cp-size-check);
203
+ border-radius: 50%;
204
+ background: var(--cp-c-brand-soft);
205
+ color: var(--cp-c-text-1);
206
+ display: grid;
207
+ place-items: center;
208
+ font-size: var(--cp-fs-check);
209
+ flex-shrink: 0;
210
+ margin-top: var(--cp-s-2);
211
+ }
212
+
213
+ /*
214
+ * On the vertical axis the CTA is a panel-level child rendered after the
215
+ * visual, so it closes the panel instead of splitting the body copy from
216
+ * the visual. Horizontal keeps its CTA inside the copy column, where the
217
+ * side-by-side columns already put it in the right place.
218
+ */
219
+ .cp-tabs--vertical .cp-tabs__panel > .cp-tabs__actions {
220
+ display: flex;
221
+ justify-content: flex-start;
222
+ }
223
+
224
+ .cp-tabs__btn {
225
+ display: inline-flex;
226
+ align-items: center;
227
+ gap: var(--cp-s-8);
228
+ padding: var(--cp-s-10) var(--cp-s-18);
229
+ font-size: var(--cp-fs-btn);
230
+ font-weight: 500;
231
+ border-radius: var(--cp-radius-pill);
232
+ border: 1px solid var(--cp-c-border);
233
+ background: transparent;
234
+ color: var(--cp-c-text-1);
235
+ cursor: pointer;
236
+ /* Named rather than `all`: `all` would animate any layout property added
237
+ later, turning a padding tweak into an unintended reflow animation. */
238
+ transition:
239
+ background var(--cp-duration-fast) var(--cp-ease-base),
240
+ border-color var(--cp-duration-fast) var(--cp-ease-base),
241
+ color var(--cp-duration-fast) var(--cp-ease-base);
242
+ text-decoration: none;
243
+ }
244
+
245
+ .cp-tabs__btn:hover {
246
+ background: var(--cp-c-bg-soft);
247
+ border-color: var(--cp-c-border);
248
+ }
249
+
250
+ /* Matches the ring on `.cp-tabs__tab` — without it the CTA fell back to the
251
+ UA default outline, which is nearly invisible on the dark themes and read
252
+ as a different focus treatment inside one component. */
253
+ .cp-tabs__btn:focus-visible {
254
+ outline: var(--cp-size-focus-ring) solid var(--cp-c-brand-1);
255
+ outline-offset: var(--cp-size-focus-ring-offset);
256
+ }
257
+
258
+ .cp-tabs__btn--primary {
259
+ background: var(--cp-c-brand-1);
260
+ color: var(--cp-c-brand-fg);
261
+ border-color: var(--cp-c-brand-1);
262
+ font-weight: 600;
263
+ }
264
+
265
+ .cp-tabs__btn--primary:hover {
266
+ background: var(--cp-c-brand-2);
267
+ border-color: var(--cp-c-brand-2);
268
+ }
269
+
270
+ /* ── Panel visual — same frame as the split band ─────────────── */
271
+ .cp-tabs__visual {
272
+ border: 1px solid var(--cp-c-border);
273
+ border-radius: var(--cp-radius-lg);
274
+ overflow: hidden;
275
+ background: var(--cp-code-block-bg);
276
+ min-width: 0;
277
+ }
278
+
279
+ .cp-tabs__visual .rp-codeblock {
280
+ margin: 0;
281
+ border-radius: 0;
282
+ }
283
+
284
+ .cp-tabs__visual .rp-codeblock pre {
285
+ overflow-x: auto;
286
+ }
287
+
288
+ .cp-tabs__visual .cp-hero-demo {
289
+ max-width: none;
290
+ border: 0;
291
+ border-radius: 0;
292
+ box-shadow: none;
293
+ }
294
+
295
+ .cp-tabs__img {
296
+ display: block;
297
+ width: 100%;
298
+ height: auto;
299
+ }