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

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.
@@ -1,5 +1,3 @@
1
- export { CopyMarkdownButton } from './copy-markdown-button'
2
- export type { CopyMarkdownButtonProps } from './copy-markdown-button'
3
1
  export { OpenAPIOperation } from './operation'
4
2
  export type { OpenAPIOperationProps } from './operation'
5
3
  export { OpenAPIOverview } from './overview'
@@ -5,20 +5,6 @@
5
5
  * Uses BEM naming: block__element--modifier.
6
6
  */
7
7
 
8
- /* ── Hide Rspress default LLMs copy button on OpenAPI pages ── */
9
- /* OpenAPI pages use a custom CopyMarkdownButton with build-time rendered markdown */
10
- :has(.cp-oas-operation) > .rp-llms-container,
11
- :has(.cp-oas-overview) > .rp-llms-container {
12
- display: none;
13
- }
14
-
15
- /* ── Copy markdown button ─────────────────────────────────── */
16
- .cp-oas-copy-markdown {
17
- float: right;
18
- margin-top: 8px;
19
- margin-bottom: 0;
20
- }
21
-
22
8
  /* ── Two-column operation layout ───────────────────────────── */
23
9
  .cp-oas-operation {
24
10
  display: grid;
@@ -1,41 +1,125 @@
1
- import catppuccin from '@iconify-json/catppuccin/icons.json' with { type: 'json' }
2
- import devicon from '@iconify-json/devicon/icons.json' with { type: 'json' }
3
- import logos from '@iconify-json/logos/icons.json' with { type: 'json' }
4
- import materialIconTheme from '@iconify-json/material-icon-theme/icons.json' with { type: 'json' }
5
- import mdi from '@iconify-json/mdi/icons.json' with { type: 'json' }
6
- import pixelarticons from '@iconify-json/pixelarticons/icons.json' with { type: 'json' }
7
- import simpleIcons from '@iconify-json/simple-icons/icons.json' with { type: 'json' }
8
- import skillIcons from '@iconify-json/skill-icons/icons.json' with { type: 'json' }
9
- import vscodeIcons from '@iconify-json/vscode-icons/icons.json' with { type: 'json' }
10
- import { addCollection, Icon } from '@iconify/react'
11
-
12
- // Register all icon collections for offline Iconify resolution.
13
- // `addCollection` is called purely for its side effect of mutating
14
- // Iconify's internal registry. Holding the return values in a
15
- // throwaway `const` keeps the call list a single expression statement
16
- // rather than nine misleading named exports.
17
- // oxlint-disable-next-line no-unused-vars
18
- const _iconCollectionsLoaded = [
19
- addCollection(cast(pixelarticons)),
20
- addCollection(cast(devicon)),
21
- addCollection(cast(mdi)),
22
- addCollection(cast(simpleIcons)),
23
- addCollection(cast(skillIcons)),
24
- addCollection(cast(catppuccin)),
25
- addCollection(cast(logos)),
26
- addCollection(cast(vscodeIcons)),
27
- addCollection(cast(materialIconTheme)),
28
- ] as const
29
-
30
- export { Icon }
1
+ import { addCollection, Icon as IconifyIcon } from '@iconify/react'
2
+ import type { IconProps } from '@iconify/react'
3
+ import type React from 'react'
4
+ import { useEffect, useState } from 'react'
31
5
 
32
6
  /**
33
- * Cast an icon JSON import to the type expected by `addCollection`.
7
+ * Per-collection lazy loaders keyed by Iconify prefix.
8
+ *
9
+ * Each entry is a bare dynamic `import()` so the consuming site's Rsbuild
10
+ * build emits **one async chunk per collection** instead of folding all nine
11
+ * `icons.json` files into a single eager ~30MB chunk pulled on every route.
12
+ * Two consequences fall out of that:
13
+ *
14
+ * - **Deployability** — the largest collection (`logos`, ~8MB) stays well
15
+ * under per-file host caps (Cloudflare Pages rejects files >25MB), where
16
+ * the combined blob failed outright.
17
+ * - **Performance** — a page only downloads the collections it actually
18
+ * references, not the full set on first paint.
19
+ *
20
+ * The specifiers are string literals (not computed) so the bundler can
21
+ * statically resolve every chunk at build time.
22
+ *
23
+ * @private
24
+ */
25
+ const COLLECTION_LOADERS: Record<string, () => Promise<{ readonly default: unknown }>> = {
26
+ catppuccin: () => import('@iconify-json/catppuccin/icons.json'),
27
+ devicon: () => import('@iconify-json/devicon/icons.json'),
28
+ logos: () => import('@iconify-json/logos/icons.json'),
29
+ 'material-icon-theme': () => import('@iconify-json/material-icon-theme/icons.json'),
30
+ mdi: () => import('@iconify-json/mdi/icons.json'),
31
+ pixelarticons: () => import('@iconify-json/pixelarticons/icons.json'),
32
+ 'simple-icons': () => import('@iconify-json/simple-icons/icons.json'),
33
+ 'skill-icons': () => import('@iconify-json/skill-icons/icons.json'),
34
+ 'vscode-icons': () => import('@iconify-json/vscode-icons/icons.json'),
35
+ }
36
+
37
+ /**
38
+ * Cache of in-flight / settled collection registrations keyed by prefix.
39
+ * Guarantees each collection's chunk is fetched and merged into Iconify's
40
+ * registry exactly once, regardless of how many `<Icon>` instances on a
41
+ * page reference it.
42
+ *
43
+ * @private
44
+ */
45
+ const collectionCache = new Map<string, Promise<void>>()
46
+
47
+ /**
48
+ * Offline-registered Iconify icon.
49
+ *
50
+ * Renders `@iconify/react`'s `Icon` unchanged, but registers the icon's
51
+ * collection on demand: the first time a prefix is seen the matching
52
+ * `@iconify-json` chunk is dynamically imported and merged into Iconify's
53
+ * registry, then a re-render paints the resolved SVG. Because `IconifyIcon`
54
+ * reads the live registry on every render, an icon appears as soon as its
55
+ * collection chunk resolves.
56
+ *
57
+ * @param props - Standard `@iconify/react` icon props; `icon` is the
58
+ * `prefix:name` identifier (e.g. `devicon:typescript`)
59
+ * @returns The Iconify icon element
60
+ */
61
+ export function Icon(props: IconProps): React.ReactElement {
62
+ const prefix = resolvePrefix(props.icon)
63
+ const [, markRegistered] = useState(false)
64
+
65
+ useEffect(() => {
66
+ ensureCollection(prefix).then(() => markRegistered(true))
67
+ }, [prefix])
68
+
69
+ return <IconifyIcon {...props} />
70
+ }
71
+
72
+ /**
73
+ * Dynamically import and register the collection for a prefix, once.
74
+ *
75
+ * Returns the cached registration promise on repeat calls so the chunk is
76
+ * fetched a single time. Unknown prefixes (no bundled collection) resolve
77
+ * immediately — `IconifyIcon` falls back to its own resolution for those.
78
+ *
79
+ * @private
80
+ * @param prefix - Iconify collection prefix (e.g. `logos`)
81
+ * @returns Promise that settles once the collection is registered
82
+ */
83
+ function ensureCollection(prefix: string): Promise<void> {
84
+ const cached = collectionCache.get(prefix)
85
+ if (cached !== undefined) {
86
+ return cached
87
+ }
88
+ const loader = COLLECTION_LOADERS[prefix]
89
+ if (loader === undefined) {
90
+ return Promise.resolve()
91
+ }
92
+ const registration = loader().then(registerModule)
93
+ collectionCache.set(prefix, registration)
94
+ return registration
95
+ }
96
+
97
+ /**
98
+ * Merge a dynamically imported `icons.json` module into Iconify's registry.
99
+ *
100
+ * @private
101
+ * @param mod - Module namespace whose `default` export is the collection JSON
102
+ */
103
+ function registerModule(mod: { readonly default: unknown }): void {
104
+ addCollection(mod.default as Parameters<typeof addCollection>[0])
105
+ }
106
+
107
+ /**
108
+ * Extract the collection prefix from an Iconify identifier. Non-string icon
109
+ * inputs and identifiers without a `prefix:name` shape yield an empty string,
110
+ * which `ensureCollection` treats as "nothing to load".
34
111
  *
35
112
  * @private
36
- * @param v - Raw icon JSON import
37
- * @returns Value cast to the addCollection parameter type
113
+ * @param icon - The `icon` prop passed to `<Icon>`
114
+ * @returns The collection prefix, or `''` when none can be determined
38
115
  */
39
- function cast(v: unknown): Parameters<typeof addCollection>[0] {
40
- return v as Parameters<typeof addCollection>[0]
116
+ function resolvePrefix(icon: IconProps['icon']): string {
117
+ if (typeof icon !== 'string') {
118
+ return ''
119
+ }
120
+ const parts = icon.split(':')
121
+ if (parts.length < 2) {
122
+ return ''
123
+ }
124
+ return parts[0]
41
125
  }
@@ -149,8 +149,6 @@ export type { StepsProps, StepProps } from './components/shared/steps'
149
149
  export { Field, FieldGroup } from './components/shared/field'
150
150
  export type { FieldProps, FieldGroupProps } from './components/shared/field'
151
151
 
152
- export { CopyMarkdownButton } from './components/openapi'
153
- export type { CopyMarkdownButtonProps } from './components/openapi'
154
152
  export { OpenAPIOperation } from './components/openapi'
155
153
  export type { OpenAPIOperationProps } from './components/openapi'
156
154
  export { OpenAPIOverview } from './components/openapi'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciderpress/ui",
3
- "version": "1.0.0-rc.4",
3
+ "version": "1.0.0-rc.6",
4
4
  "description": "Rspress plugin, theme components, and styles for ciderpress",
5
5
  "keywords": [
6
6
  "ciderpress",
@@ -63,7 +63,7 @@
63
63
  "react-aria-components": "^1.19.0",
64
64
  "ts-morph": "^28.0.0",
65
65
  "unist-util-visit": "^5.1.0",
66
- "@ciderpress/config": "1.0.0-rc.4",
66
+ "@ciderpress/config": "1.0.0-rc.5",
67
67
  "@ciderpress/theme": "1.0.0-rc.3"
68
68
  },
69
69
  "devDependencies": {
@@ -1,5 +1,3 @@
1
- export { CopyMarkdownButton } from './copy-markdown-button'
2
- export type { CopyMarkdownButtonProps } from './copy-markdown-button'
3
1
  export { OpenAPIOperation } from './operation'
4
2
  export type { OpenAPIOperationProps } from './operation'
5
3
  export { OpenAPIOverview } from './overview'
@@ -5,20 +5,6 @@
5
5
  * Uses BEM naming: block__element--modifier.
6
6
  */
7
7
 
8
- /* ── Hide Rspress default LLMs copy button on OpenAPI pages ── */
9
- /* OpenAPI pages use a custom CopyMarkdownButton with build-time rendered markdown */
10
- :has(.cp-oas-operation) > .rp-llms-container,
11
- :has(.cp-oas-overview) > .rp-llms-container {
12
- display: none;
13
- }
14
-
15
- /* ── Copy markdown button ─────────────────────────────────── */
16
- .cp-oas-copy-markdown {
17
- float: right;
18
- margin-top: 8px;
19
- margin-bottom: 0;
20
- }
21
-
22
8
  /* ── Two-column operation layout ───────────────────────────── */
23
9
  .cp-oas-operation {
24
10
  display: grid;
@@ -1,41 +1,125 @@
1
- import catppuccin from '@iconify-json/catppuccin/icons.json' with { type: 'json' }
2
- import devicon from '@iconify-json/devicon/icons.json' with { type: 'json' }
3
- import logos from '@iconify-json/logos/icons.json' with { type: 'json' }
4
- import materialIconTheme from '@iconify-json/material-icon-theme/icons.json' with { type: 'json' }
5
- import mdi from '@iconify-json/mdi/icons.json' with { type: 'json' }
6
- import pixelarticons from '@iconify-json/pixelarticons/icons.json' with { type: 'json' }
7
- import simpleIcons from '@iconify-json/simple-icons/icons.json' with { type: 'json' }
8
- import skillIcons from '@iconify-json/skill-icons/icons.json' with { type: 'json' }
9
- import vscodeIcons from '@iconify-json/vscode-icons/icons.json' with { type: 'json' }
10
- import { addCollection, Icon } from '@iconify/react'
11
-
12
- // Register all icon collections for offline Iconify resolution.
13
- // `addCollection` is called purely for its side effect of mutating
14
- // Iconify's internal registry. Holding the return values in a
15
- // throwaway `const` keeps the call list a single expression statement
16
- // rather than nine misleading named exports.
17
- // oxlint-disable-next-line no-unused-vars
18
- const _iconCollectionsLoaded = [
19
- addCollection(cast(pixelarticons)),
20
- addCollection(cast(devicon)),
21
- addCollection(cast(mdi)),
22
- addCollection(cast(simpleIcons)),
23
- addCollection(cast(skillIcons)),
24
- addCollection(cast(catppuccin)),
25
- addCollection(cast(logos)),
26
- addCollection(cast(vscodeIcons)),
27
- addCollection(cast(materialIconTheme)),
28
- ] as const
29
-
30
- export { Icon }
1
+ import { addCollection, Icon as IconifyIcon } from '@iconify/react'
2
+ import type { IconProps } from '@iconify/react'
3
+ import type React from 'react'
4
+ import { useEffect, useState } from 'react'
31
5
 
32
6
  /**
33
- * Cast an icon JSON import to the type expected by `addCollection`.
7
+ * Per-collection lazy loaders keyed by Iconify prefix.
8
+ *
9
+ * Each entry is a bare dynamic `import()` so the consuming site's Rsbuild
10
+ * build emits **one async chunk per collection** instead of folding all nine
11
+ * `icons.json` files into a single eager ~30MB chunk pulled on every route.
12
+ * Two consequences fall out of that:
13
+ *
14
+ * - **Deployability** — the largest collection (`logos`, ~8MB) stays well
15
+ * under per-file host caps (Cloudflare Pages rejects files >25MB), where
16
+ * the combined blob failed outright.
17
+ * - **Performance** — a page only downloads the collections it actually
18
+ * references, not the full set on first paint.
19
+ *
20
+ * The specifiers are string literals (not computed) so the bundler can
21
+ * statically resolve every chunk at build time.
22
+ *
23
+ * @private
24
+ */
25
+ const COLLECTION_LOADERS: Record<string, () => Promise<{ readonly default: unknown }>> = {
26
+ catppuccin: () => import('@iconify-json/catppuccin/icons.json'),
27
+ devicon: () => import('@iconify-json/devicon/icons.json'),
28
+ logos: () => import('@iconify-json/logos/icons.json'),
29
+ 'material-icon-theme': () => import('@iconify-json/material-icon-theme/icons.json'),
30
+ mdi: () => import('@iconify-json/mdi/icons.json'),
31
+ pixelarticons: () => import('@iconify-json/pixelarticons/icons.json'),
32
+ 'simple-icons': () => import('@iconify-json/simple-icons/icons.json'),
33
+ 'skill-icons': () => import('@iconify-json/skill-icons/icons.json'),
34
+ 'vscode-icons': () => import('@iconify-json/vscode-icons/icons.json'),
35
+ }
36
+
37
+ /**
38
+ * Cache of in-flight / settled collection registrations keyed by prefix.
39
+ * Guarantees each collection's chunk is fetched and merged into Iconify's
40
+ * registry exactly once, regardless of how many `<Icon>` instances on a
41
+ * page reference it.
42
+ *
43
+ * @private
44
+ */
45
+ const collectionCache = new Map<string, Promise<void>>()
46
+
47
+ /**
48
+ * Offline-registered Iconify icon.
49
+ *
50
+ * Renders `@iconify/react`'s `Icon` unchanged, but registers the icon's
51
+ * collection on demand: the first time a prefix is seen the matching
52
+ * `@iconify-json` chunk is dynamically imported and merged into Iconify's
53
+ * registry, then a re-render paints the resolved SVG. Because `IconifyIcon`
54
+ * reads the live registry on every render, an icon appears as soon as its
55
+ * collection chunk resolves.
56
+ *
57
+ * @param props - Standard `@iconify/react` icon props; `icon` is the
58
+ * `prefix:name` identifier (e.g. `devicon:typescript`)
59
+ * @returns The Iconify icon element
60
+ */
61
+ export function Icon(props: IconProps): React.ReactElement {
62
+ const prefix = resolvePrefix(props.icon)
63
+ const [, markRegistered] = useState(false)
64
+
65
+ useEffect(() => {
66
+ ensureCollection(prefix).then(() => markRegistered(true))
67
+ }, [prefix])
68
+
69
+ return <IconifyIcon {...props} />
70
+ }
71
+
72
+ /**
73
+ * Dynamically import and register the collection for a prefix, once.
74
+ *
75
+ * Returns the cached registration promise on repeat calls so the chunk is
76
+ * fetched a single time. Unknown prefixes (no bundled collection) resolve
77
+ * immediately — `IconifyIcon` falls back to its own resolution for those.
78
+ *
79
+ * @private
80
+ * @param prefix - Iconify collection prefix (e.g. `logos`)
81
+ * @returns Promise that settles once the collection is registered
82
+ */
83
+ function ensureCollection(prefix: string): Promise<void> {
84
+ const cached = collectionCache.get(prefix)
85
+ if (cached !== undefined) {
86
+ return cached
87
+ }
88
+ const loader = COLLECTION_LOADERS[prefix]
89
+ if (loader === undefined) {
90
+ return Promise.resolve()
91
+ }
92
+ const registration = loader().then(registerModule)
93
+ collectionCache.set(prefix, registration)
94
+ return registration
95
+ }
96
+
97
+ /**
98
+ * Merge a dynamically imported `icons.json` module into Iconify's registry.
99
+ *
100
+ * @private
101
+ * @param mod - Module namespace whose `default` export is the collection JSON
102
+ */
103
+ function registerModule(mod: { readonly default: unknown }): void {
104
+ addCollection(mod.default as Parameters<typeof addCollection>[0])
105
+ }
106
+
107
+ /**
108
+ * Extract the collection prefix from an Iconify identifier. Non-string icon
109
+ * inputs and identifiers without a `prefix:name` shape yield an empty string,
110
+ * which `ensureCollection` treats as "nothing to load".
34
111
  *
35
112
  * @private
36
- * @param v - Raw icon JSON import
37
- * @returns Value cast to the addCollection parameter type
113
+ * @param icon - The `icon` prop passed to `<Icon>`
114
+ * @returns The collection prefix, or `''` when none can be determined
38
115
  */
39
- function cast(v: unknown): Parameters<typeof addCollection>[0] {
40
- return v as Parameters<typeof addCollection>[0]
116
+ function resolvePrefix(icon: IconProps['icon']): string {
117
+ if (typeof icon !== 'string') {
118
+ return ''
119
+ }
120
+ const parts = icon.split(':')
121
+ if (parts.length < 2) {
122
+ return ''
123
+ }
124
+ return parts[0]
41
125
  }
@@ -149,8 +149,6 @@ export type { StepsProps, StepProps } from './components/shared/steps'
149
149
  export { Field, FieldGroup } from './components/shared/field'
150
150
  export type { FieldProps, FieldGroupProps } from './components/shared/field'
151
151
 
152
- export { CopyMarkdownButton } from './components/openapi'
153
- export type { CopyMarkdownButtonProps } from './components/openapi'
154
152
  export { OpenAPIOperation } from './components/openapi'
155
153
  export type { OpenAPIOperationProps } from './components/openapi'
156
154
  export { OpenAPIOverview } from './components/openapi'
@@ -1,41 +0,0 @@
1
- import { LlmsCopyButton } from '@rspress/core/theme'
2
- import type React from 'react'
3
- import { useCallback } from 'react'
4
-
5
- export interface CopyMarkdownButtonProps {
6
- /**
7
- * Markdown content to copy to the clipboard.
8
- */
9
- readonly markdown: string
10
- }
11
-
12
- /**
13
- * Copy Markdown button that overrides Rspress's default copy behavior.
14
- *
15
- * Uses the native Clipboard API to copy pre-generated markdown content
16
- * instead of the raw MDX source.
17
- *
18
- * @param props - Props with the markdown string to copy to the clipboard
19
- * @returns React element with the copy button
20
- */
21
- export function CopyMarkdownButton({
22
- markdown,
23
- }: CopyMarkdownButtonProps): React.ReactElement | null {
24
- if (import.meta.env.SSG_MD) {
25
- return null
26
- }
27
-
28
- const handleClick = useCallback(
29
- (event: React.MouseEvent<HTMLButtonElement>) => {
30
- event.preventDefault()
31
- return navigator.clipboard.writeText(markdown).catch(() => null)
32
- },
33
- [markdown]
34
- )
35
-
36
- return (
37
- <div className="cp-oas-copy-markdown">
38
- <LlmsCopyButton text="Copy Markdown" onClick={handleClick} />
39
- </div>
40
- )
41
- }
@@ -1,41 +0,0 @@
1
- import { LlmsCopyButton } from '@rspress/core/theme'
2
- import type React from 'react'
3
- import { useCallback } from 'react'
4
-
5
- export interface CopyMarkdownButtonProps {
6
- /**
7
- * Markdown content to copy to the clipboard.
8
- */
9
- readonly markdown: string
10
- }
11
-
12
- /**
13
- * Copy Markdown button that overrides Rspress's default copy behavior.
14
- *
15
- * Uses the native Clipboard API to copy pre-generated markdown content
16
- * instead of the raw MDX source.
17
- *
18
- * @param props - Props with the markdown string to copy to the clipboard
19
- * @returns React element with the copy button
20
- */
21
- export function CopyMarkdownButton({
22
- markdown,
23
- }: CopyMarkdownButtonProps): React.ReactElement | null {
24
- if (import.meta.env.SSG_MD) {
25
- return null
26
- }
27
-
28
- const handleClick = useCallback(
29
- (event: React.MouseEvent<HTMLButtonElement>) => {
30
- event.preventDefault()
31
- return navigator.clipboard.writeText(markdown).catch(() => null)
32
- },
33
- [markdown]
34
- )
35
-
36
- return (
37
- <div className="cp-oas-copy-markdown">
38
- <LlmsCopyButton text="Copy Markdown" onClick={handleClick} />
39
- </div>
40
- )
41
- }