@ciderpress/ui 1.0.0-rc.1 → 1.0.0-rc.4

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 (67) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.mjs +4 -3
  3. package/dist/node.d.ts +1 -1
  4. package/dist/node.mjs +316 -50
  5. package/dist/theme/components/footer/site-footer.tsx +68 -9
  6. package/dist/theme/components/home/feature-card.tsx +5 -8
  7. package/dist/theme/components/home/feature.tsx +31 -11
  8. package/dist/theme/components/home/hero-demo-custom.tsx +116 -0
  9. package/dist/theme/components/home/hero-demo.css +27 -0
  10. package/dist/theme/components/home/layout.tsx +149 -26
  11. package/dist/theme/components/home/split-visual-custom.tsx +26 -0
  12. package/dist/theme/components/home/workspaces.tsx +39 -3
  13. package/dist/theme/components/nav/branch-tag.tsx +7 -7
  14. package/dist/theme/components/nav/ciderpress-header.css +2 -1
  15. package/dist/theme/components/nav/ciderpress-header.tsx +26 -3
  16. package/dist/theme/components/nav/ciderpress-nav-social-links.tsx +9 -7
  17. package/dist/theme/components/nav/floating-branch-indicator.tsx +7 -7
  18. package/dist/theme/components/nav/header-icon.css +24 -0
  19. package/dist/theme/components/nav/header-icon.tsx +119 -0
  20. package/dist/theme/components/nav/header-logo.css +16 -0
  21. package/dist/theme/components/nav/header-logo.tsx +200 -0
  22. package/dist/theme/components/nav/layout.tsx +45 -7
  23. package/dist/theme/components/nav/nav-logo.tsx +16 -19
  24. package/dist/theme/components/shared/card-icon.tsx +37 -0
  25. package/dist/theme/components/shared/ciderpress-logo.tsx +5 -1
  26. package/dist/theme/components/shared/resolve-card-icon.ts +53 -12
  27. package/dist/theme/components/shared/section-card.tsx +9 -6
  28. package/dist/theme/components/sidebar/sidebar-links.tsx +6 -2
  29. package/dist/theme/components/theme-provider.tsx +27 -11
  30. package/dist/theme/components/workspaces/card.tsx +7 -9
  31. package/dist/theme/hooks/use-ciderpress.ts +60 -5
  32. package/dist/theme/index.tsx +0 -1
  33. package/dist/theme/lib/read-social-links.ts +4 -3
  34. package/dist/theme/lib/theme-favicon.ts +1 -1
  35. package/dist/theme/lib/with-mount-base.ts +31 -0
  36. package/package.json +14 -14
  37. package/src/theme/components/footer/site-footer.tsx +68 -9
  38. package/src/theme/components/home/feature-card.tsx +5 -8
  39. package/src/theme/components/home/feature.tsx +31 -11
  40. package/src/theme/components/home/hero-demo-custom.tsx +116 -0
  41. package/src/theme/components/home/hero-demo.css +27 -0
  42. package/src/theme/components/home/layout.tsx +149 -26
  43. package/src/theme/components/home/split-visual-custom.tsx +26 -0
  44. package/src/theme/components/home/workspaces.tsx +39 -3
  45. package/src/theme/components/nav/branch-tag.tsx +7 -7
  46. package/src/theme/components/nav/ciderpress-header.css +2 -1
  47. package/src/theme/components/nav/ciderpress-header.tsx +26 -3
  48. package/src/theme/components/nav/ciderpress-nav-social-links.tsx +9 -7
  49. package/src/theme/components/nav/floating-branch-indicator.tsx +7 -7
  50. package/src/theme/components/nav/header-icon.css +24 -0
  51. package/src/theme/components/nav/header-icon.tsx +119 -0
  52. package/src/theme/components/nav/header-logo.css +16 -0
  53. package/src/theme/components/nav/header-logo.tsx +200 -0
  54. package/src/theme/components/nav/layout.tsx +45 -7
  55. package/src/theme/components/nav/nav-logo.tsx +16 -19
  56. package/src/theme/components/shared/card-icon.tsx +37 -0
  57. package/src/theme/components/shared/ciderpress-logo.tsx +5 -1
  58. package/src/theme/components/shared/resolve-card-icon.ts +53 -12
  59. package/src/theme/components/shared/section-card.tsx +9 -6
  60. package/src/theme/components/sidebar/sidebar-links.tsx +6 -2
  61. package/src/theme/components/theme-provider.tsx +27 -11
  62. package/src/theme/components/workspaces/card.tsx +7 -9
  63. package/src/theme/hooks/use-ciderpress.ts +60 -5
  64. package/src/theme/index.tsx +0 -1
  65. package/src/theme/lib/read-social-links.ts +4 -3
  66. package/src/theme/lib/theme-favicon.ts +1 -1
  67. package/src/theme/lib/with-mount-base.ts +31 -0
@@ -2,14 +2,15 @@ import { match, P } from 'massaman/match'
2
2
  import type React from 'react'
3
3
 
4
4
  import { Card } from './card'
5
- import { Icon } from './icon'
5
+ import { CardIcon } from './card-icon'
6
+ import type { CardIconInput } from './resolve-card-icon'
6
7
  import { resolveCardIcon } from './resolve-card-icon'
7
8
 
8
9
  export interface SectionCardProps {
9
10
  readonly href: string
10
11
  readonly title: string
11
12
  readonly description?: string
12
- readonly icon?: string | { readonly id: string; readonly color: string }
13
+ readonly icon?: CardIconInput
13
14
  }
14
15
 
15
16
  /**
@@ -25,7 +26,11 @@ export function SectionCard({
25
26
  description,
26
27
  icon = 'pixelarticons:file',
27
28
  }: SectionCardProps): React.ReactElement {
28
- const resolved = resolveCardIcon(icon) ?? { id: 'pixelarticons:file', color: 'purple' }
29
+ const resolved = resolveCardIcon(icon) ?? {
30
+ kind: 'iconify' as const,
31
+ id: 'pixelarticons:file',
32
+ color: 'purple',
33
+ }
29
34
  const descEl = match(description)
30
35
  .with(P.nonNullable, (d) => <span className="cp-section-card__desc">{d}</span>)
31
36
  .otherwise(() => null)
@@ -33,9 +38,7 @@ export function SectionCard({
33
38
  return (
34
39
  <Card href={href} className="cp-section-card">
35
40
  <div className="cp-section-card__header">
36
- <span className={`cp-section-card__icon cp-section-card__icon--${resolved.color}`}>
37
- <Icon icon={resolved.id} />
38
- </span>
41
+ <CardIcon resolved={resolved} className="cp-section-card__icon" />
39
42
  <span className="cp-section-card__title">{title}</span>
40
43
  </div>
41
44
  {descEl}
@@ -1,3 +1,4 @@
1
+ import type { SerializedIcon } from '@ciderpress/config'
1
2
  import { Link } from '@rspress/core/runtime'
2
3
  import { match, P } from 'massaman/match'
3
4
  import type React from 'react'
@@ -10,7 +11,7 @@ import './sidebar-links.css'
10
11
  interface SidebarLinkItem {
11
12
  readonly text: string
12
13
  readonly link: string
13
- readonly icon?: string | { readonly id: string; readonly color: string }
14
+ readonly icon?: SerializedIcon
14
15
  readonly style?: 'brand' | 'alt' | 'ghost'
15
16
  readonly shape?: 'square' | 'rounded' | 'circle'
16
17
  }
@@ -51,7 +52,10 @@ export function SidebarLinks(props: SidebarLinksProps): React.ReactElement | nul
51
52
  function renderIcon(icon: SidebarLinkItem['icon']): React.ReactElement | null {
52
53
  return match(icon)
53
54
  .with(P.string, (id) => <Icon icon={id} className="cp-sidebar-link-icon" />)
54
- .with({ id: P.string, color: P.string }, (i) => (
55
+ .with({ kind: 'image' }, (img) => (
56
+ <img src={img.src} alt={img.alt} className="cp-sidebar-link-icon cp-sidebar-link-icon--img" />
57
+ ))
58
+ .with({ id: P.string }, (i) => (
55
59
  <Icon icon={i.id} className="cp-sidebar-link-icon" style={{ color: i.color }} />
56
60
  ))
57
61
  .otherwise(() => null)
@@ -11,6 +11,8 @@ declare const __CIDERPRESS_DEFAULT_VARIANT__: string
11
11
  declare const __CIDERPRESS_THEME_COLORS__: string
12
12
  declare const __CIDERPRESS_THEME_DARK_COLORS__: string
13
13
  declare const __CIDERPRESS_THEME_REGISTRY__: string
14
+ declare const __CIDERPRESS_HAS_USER_FAVICON__: boolean
15
+ declare const __CIDERPRESS_LOADER_MIN_MS__: number
14
16
 
15
17
  interface RegistryEntry {
16
18
  readonly name: string
@@ -26,7 +28,7 @@ type ThemeColorKey = keyof ThemeColors
26
28
 
27
29
  /**
28
30
  * Parsed theme registry — built-in themes plus any user themes from
29
- * `config.themes`. Read from the build-time define so the client bundle
31
+ * `theme.themes`. Read from the build-time define so the client bundle
30
32
  * does not pull `@ciderpress/theme`'s factory + Zod into the runtime path.
31
33
  */
32
34
  const REGISTRY_ENTRIES: readonly RegistryEntry[] = parseRegistry(__CIDERPRESS_THEME_REGISTRY__)
@@ -123,9 +125,11 @@ const COLOR_VAR_MAP: Readonly<Record<ThemeColorKey, readonly string[]>> = Object
123
125
  const ALL_CSS_VARS: readonly string[] = Object.values(COLOR_VAR_MAP).flat()
124
126
 
125
127
  /**
126
- * Minimum time (ms) the loading overlay stays visible before fading out.
128
+ * Minimum time (ms) the loading overlay stays visible before fading
129
+ * out. Resolved at build time from `brand.loader.minDisplayMs` —
130
+ * defaults to 150ms in `packages/ui/src/config.ts`.
127
131
  */
128
- const LOADER_MIN_DISPLAY_MS = 150
132
+ const LOADER_MIN_DISPLAY_MS = __CIDERPRESS_LOADER_MIN_MS__
129
133
 
130
134
  /**
131
135
  * Duration (ms) of the CSS fade-out transition. Must match the
@@ -199,12 +203,14 @@ export function ThemeProvider(): React.ReactElement | null {
199
203
  applyColorOverrides(html, darkColors)
200
204
  }
201
205
 
202
- // Sync the document favicon with the active theme's brand colour.
203
- // Browsers cache static icon assets and ignore CSS, so the only way to
204
- // keep the tab mark in lockstep with the chosen theme is to swap
205
- // `<link rel="icon">` to a data-URI SVG carrying the resolved
206
- // `--cp-c-brand-1`.
207
- syncThemeFavicon(html)
206
+ // Sync the document favicon with the active theme's brand colour
207
+ // only when the user has NOT set their own `favicon` in config. When
208
+ // they have, Rspress already points `<link rel="icon">` at the
209
+ // user's asset, and overwriting it with the themed apple data-URI
210
+ // would silently restore ciderpress branding.
211
+ if (!__CIDERPRESS_HAS_USER_FAVICON__) {
212
+ syncThemeFavicon(html)
213
+ }
208
214
 
209
215
  // Observe `.rp-dark` class changes so Rspress's built-in dark toggle
210
216
  // stays the single source of truth for variant flips. The new variant
@@ -253,8 +259,11 @@ export function ThemeProvider(): React.ReactElement | null {
253
259
  }
254
260
  // Re-sync the favicon — brand colour is constant across variants for
255
261
  // built-in themes, but surface overrides on the dark variant can
256
- // affect the chip background colour we bake into the SVG.
257
- syncThemeFavicon(html)
262
+ // affect the chip background colour we bake into the SVG. Skipped
263
+ // when the user supplied their own favicon (see initial sync above).
264
+ if (!__CIDERPRESS_HAS_USER_FAVICON__) {
265
+ syncThemeFavicon(html)
266
+ }
258
267
  })
259
268
  observer.observe(html, { attributes: true, attributeFilter: ['class'] })
260
269
 
@@ -628,6 +637,13 @@ function dismissLoader(html: HTMLElement): () => void {
628
637
  clearDotsInterval()
629
638
  }, LOADER_MIN_DISPLAY_MS + LOADER_FADE_MS)
630
639
 
640
+ // The forced-dismiss fallback (covering the case where React never
641
+ // hydrates) lives in the inline head script — see `buildHeadScriptBody`
642
+ // in `packages/ui/src/config.ts`. That timer always runs, so a
643
+ // duplicate React-side fallback here would only fire AFTER ThemeProvider
644
+ // has already mounted — at which point the normal fade path above has
645
+ // already won.
646
+
631
647
  return () => {
632
648
  clearTimeout(fadeTimer)
633
649
  clearTimeout(removeTimer)
@@ -2,7 +2,8 @@ import { match, P } from 'massaman/match'
2
2
  import type React from 'react'
3
3
 
4
4
  import { Card } from '../shared/card'
5
- import { Icon } from '../shared/icon'
5
+ import { CardIcon } from '../shared/card-icon'
6
+ import type { CardIconInput } from '../shared/resolve-card-icon'
6
7
  import { resolveCardIcon } from '../shared/resolve-card-icon'
7
8
  import { TechTag } from '../shared/tech-tag'
8
9
 
@@ -16,9 +17,10 @@ export interface WorkspaceCardProps {
16
17
  */
17
18
  readonly href: string
18
19
  /**
19
- * Icon config — Iconify identifier string or `{ id, color }` object.
20
+ * Icon config — Iconify identifier string, `{ id, color }` object, or
21
+ * `{ kind: 'image', src, alt }` for a static image asset.
20
22
  */
21
- readonly icon?: string | { readonly id: string; readonly color: string }
23
+ readonly icon?: CardIconInput
22
24
  /**
23
25
  * Scope prefix shown above the name (e.g. "apps/").
24
26
  */
@@ -67,13 +69,9 @@ export function WorkspaceCard({
67
69
  const resolved = resolveCardIcon(icon)
68
70
 
69
71
  const iconEl = match(resolved)
70
- .with(P.nonNullable, (r) => <Icon icon={r.id} />)
72
+ .with(P.nonNullable, (r) => <CardIcon resolved={r} className="cp-card__icon" />)
71
73
  .otherwise(() => null)
72
74
 
73
- const iconColor = match(resolved)
74
- .with(P.nonNullable, (r) => r.color)
75
- .otherwise(() => 'purple')
76
-
77
75
  const scopeEl = match(scope)
78
76
  .with(
79
77
  P.when((s): s is string => s !== undefined && s.length > 0),
@@ -117,7 +115,7 @@ export function WorkspaceCard({
117
115
  <Card href={href} className="cp-workspace-card">
118
116
  <div className="cp-workspace-card__header">
119
117
  <div className="cp-workspace-card__identity">
120
- <span className={`cp-card__icon cp-card__icon--${iconColor}`}>{iconEl}</span>
118
+ {iconEl}
121
119
  <div className="cp-workspace-card__title">
122
120
  {scopeEl}
123
121
  <span
@@ -1,17 +1,17 @@
1
- import type { FooterConfig, HomeConfig, SiteConfig } from '@ciderpress/config'
1
+ import type { FooterConfig, HomeConfig, SerializedIcon } from '@ciderpress/config'
2
2
  import { useSite } from '@rspress/core/runtime'
3
3
 
4
4
  export interface CiderpressSidebarItem {
5
5
  readonly text?: string
6
6
  readonly link?: string
7
- readonly icon?: string
7
+ readonly icon?: SerializedIcon
8
8
  readonly items?: readonly CiderpressSidebarItem[]
9
9
  }
10
10
 
11
11
  export interface CiderpressSidebarLink {
12
12
  readonly text: string
13
13
  readonly link: string
14
- readonly icon?: string | { readonly id: string; readonly color: string }
14
+ readonly icon?: SerializedIcon
15
15
  readonly style?: 'brand' | 'alt' | 'ghost'
16
16
  readonly shape?: 'square' | 'rounded' | 'circle'
17
17
  }
@@ -19,7 +19,7 @@ export interface CiderpressSidebarLink {
19
19
  export interface WorkspaceCardData {
20
20
  readonly title: string
21
21
  readonly href: string
22
- readonly icon: string | { readonly id: string; readonly color: string } | undefined
22
+ readonly icon: SerializedIcon | undefined
23
23
  readonly scope: string | undefined
24
24
  readonly description: string | undefined
25
25
  readonly tags: readonly string[]
@@ -33,6 +33,61 @@ export interface WorkspaceGroupData {
33
33
  readonly cards: readonly WorkspaceCardData[]
34
34
  }
35
35
 
36
+ /**
37
+ * Serialised view of the unified `CiderpressConfig` that the runtime
38
+ * theme components still read through. The `site` field is rebuilt in
39
+ * `packages/ui/src/config.ts` (`buildSiteBlock`) from the new top-level
40
+ * `editLink` / `reportLink` / `topbar` / `sidebar` / `footer` / `version`
41
+ * config — kept under the legacy `site.*` name so theme components don't
42
+ * need to be rewired to look up each field individually.
43
+ */
44
+ export interface CiderpressSiteBlock {
45
+ readonly version: string | undefined
46
+ readonly edit:
47
+ | {
48
+ readonly repo: string
49
+ readonly branch?: string
50
+ readonly directory?: string
51
+ readonly label?: string
52
+ }
53
+ | undefined
54
+ readonly report:
55
+ | {
56
+ readonly repo: string
57
+ readonly branch?: string
58
+ readonly directory?: string
59
+ readonly label?: string
60
+ }
61
+ | undefined
62
+ readonly topbarCta: { readonly text: string; readonly href: string } | undefined
63
+ readonly sidebarPromo:
64
+ | {
65
+ readonly title: string
66
+ readonly body: string
67
+ readonly cta: { readonly text: string; readonly href: string }
68
+ }
69
+ | undefined
70
+ readonly announcement:
71
+ | {
72
+ readonly id?: string
73
+ readonly lead?: string
74
+ readonly message: string
75
+ readonly cta?: { readonly href: string; readonly label: string }
76
+ readonly persistent?: boolean
77
+ }
78
+ | undefined
79
+ readonly footer:
80
+ | {
81
+ readonly columns?: readonly {
82
+ readonly heading: string
83
+ readonly links: readonly { readonly text: string; readonly href: string }[]
84
+ }[]
85
+ readonly tagline?: string
86
+ readonly brandMark?: string
87
+ }
88
+ | undefined
89
+ }
90
+
36
91
  interface CiderpressThemeConfig {
37
92
  readonly sidebar: Record<string, readonly CiderpressSidebarItem[]>
38
93
  readonly sidebarAbove: readonly CiderpressSidebarLink[] | undefined
@@ -41,7 +96,7 @@ interface CiderpressThemeConfig {
41
96
  readonly standaloneScopePaths: readonly string[] | undefined
42
97
  readonly home: HomeConfig | undefined
43
98
  readonly ciderpressFooter: FooterConfig | undefined
44
- readonly site: SiteConfig | undefined
99
+ readonly site: CiderpressSiteBlock | undefined
45
100
  }
46
101
 
47
102
  /**
@@ -19,7 +19,6 @@ import '@rspress/core/dist/theme/components/Sidebar/SidebarDivider.css'
19
19
  import '@rspress/core/dist/theme/components/Sidebar/SidebarGroup.css'
20
20
  import '@rspress/core/dist/theme/components/Sidebar/SidebarItem.css'
21
21
  import '@rspress/core/dist/theme/components/Sidebar/SidebarSectionHeader.css'
22
-
23
22
  import './styles/layers.css'
24
23
  import './styles/overrides/fonts.css'
25
24
  import './styles/overrides/tokens.css'
@@ -2,8 +2,9 @@ import type { CiderpressSocialLink } from '../components/nav/ciderpress-nav-soci
2
2
 
3
3
  /**
4
4
  * Pull social-link entries (GitHub, npm, etc) from Rspress's `site`
5
- * shape. Lives on `site.themeConfig.socialLinks` after Rspress
6
- * normalisation but isn't surfaced on the public typings.
5
+ * shape. Lives on `site.themeConfig.socialLinks` after the
6
+ * `packages/ui/src/config.ts` serialiser writes the unified `socials`
7
+ * config into the runtime theme config.
7
8
  *
8
9
  * @param site - Rspress site data (typically the result of `useSite().site`)
9
10
  * @returns Array of social-link entries (empty array when not configured)
@@ -22,6 +23,6 @@ export function readSocialLinks(site: unknown): readonly CiderpressSocialLink[]
22
23
  typeof item === 'object' &&
23
24
  item !== null &&
24
25
  typeof (item as { icon?: unknown }).icon === 'string' &&
25
- typeof (item as { content?: unknown }).content === 'string'
26
+ typeof (item as { url?: unknown }).url === 'string'
26
27
  )
27
28
  }
@@ -107,7 +107,7 @@ function buildIconDataUri(brand: string, shadow: string, surface: string): strin
107
107
  /**
108
108
  * Locate the existing `<link rel="icon">` element, creating one if the
109
109
  * document head doesn't have one yet (a defensive fallback — Rspress
110
- * normally renders one from `config.icon`).
110
+ * normally renders one from `brand.favicon`).
111
111
  *
112
112
  * @private
113
113
  * @returns Mutable link element
@@ -0,0 +1,31 @@
1
+ import { withBase } from '@rspress/core/runtime'
2
+
3
+ /**
4
+ * Prefix `value` with the configured Rspress `base` only when it's an
5
+ * absolute path that would otherwise escape the mount. Pass-through for
6
+ * data URIs, fully-qualified URLs, hash anchors, and relative paths —
7
+ * applying `withBase()` to those produces broken results (the kitchen-
8
+ * sink data-URI logo regression that the examples e2e caught).
9
+ *
10
+ * Use this wrapper at every render site that takes a user-supplied asset
11
+ * URL from the ciderpress config (`logo`, `heroDemo.src`, brand chips
12
+ * etc.), instead of calling `withBase()` directly.
13
+ *
14
+ * @param value - Asset URL or path from user config (may be `undefined`)
15
+ * @returns The base-prefixed URL when applicable, otherwise `value` as-is
16
+ */
17
+ export function withMountBase(value: string | undefined): string {
18
+ if (value === undefined) {
19
+ return ''
20
+ }
21
+ if (!value.startsWith('/')) {
22
+ return value
23
+ }
24
+ // Data URIs (`data:image/svg+xml;...`) and protocol-relative or
25
+ // explicit-protocol URLs are absolute in their own namespace — never
26
+ // prefix.
27
+ if (value.startsWith('//') || /^[a-z][a-z0-9+\-.]*:/i.test(value)) {
28
+ return value
29
+ }
30
+ return withBase(value)
31
+ }