@marlinjai/clearify 1.7.2 → 1.8.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.
@@ -4,8 +4,9 @@ import {
4
4
  import {
5
5
  loadUserConfig,
6
6
  resolveConfig,
7
- resolveSections
8
- } from "./chunk-WTT3TFBP.js";
7
+ resolveSections,
8
+ scanHubProjects
9
+ } from "./chunk-YMRRZEYA.js";
9
10
  import {
10
11
  buildSectionData
11
12
  } from "./chunk-6PUBRHS7.js";
@@ -26,6 +27,8 @@ var VIRTUAL_MERMAID_SVGS = "virtual:clearify/mermaid-svgs";
26
27
  var RESOLVED_VIRTUAL_MERMAID_SVGS = "\0" + VIRTUAL_MERMAID_SVGS;
27
28
  var VIRTUAL_OPENAPI_SPEC = "virtual:clearify/openapi-spec";
28
29
  var RESOLVED_VIRTUAL_OPENAPI_SPEC = "\0" + VIRTUAL_OPENAPI_SPEC;
30
+ var VIRTUAL_HUB = "virtual:clearify/hub";
31
+ var RESOLVED_VIRTUAL_HUB = "\0" + VIRTUAL_HUB;
29
32
  function findFirstNavPath(items) {
30
33
  for (const item of items) {
31
34
  if (item.path) return item.path;
@@ -218,6 +221,7 @@ ${routeEntries.join(",\n")}
218
221
  userRoot = options.root ?? process.cwd();
219
222
  const userConfig = await loadUserConfig(userRoot);
220
223
  config = resolveConfig(userConfig, userRoot);
224
+ config = await scanHubProjects(config, userRoot);
221
225
  const allSections = resolveSections(config, userRoot);
222
226
  const isDev = resolvedViteConfig.command === "serve";
223
227
  visibleSections = isDev ? allSections : allSections.filter((s) => !s.draft);
@@ -230,6 +234,7 @@ ${routeEntries.join(",\n")}
230
234
  if (id === VIRTUAL_SEARCH_INDEX) return RESOLVED_VIRTUAL_SEARCH_INDEX;
231
235
  if (id === VIRTUAL_MERMAID_SVGS) return RESOLVED_VIRTUAL_MERMAID_SVGS;
232
236
  if (id === VIRTUAL_OPENAPI_SPEC) return RESOLVED_VIRTUAL_OPENAPI_SPEC;
237
+ if (id === VIRTUAL_HUB) return RESOLVED_VIRTUAL_HUB;
233
238
  return null;
234
239
  },
235
240
  async load(id) {
@@ -251,6 +256,10 @@ ${routeEntries.join(",\n")}
251
256
  if (id === RESOLVED_VIRTUAL_OPENAPI_SPEC) {
252
257
  return await loadOpenAPISpec();
253
258
  }
259
+ if (id === RESOLVED_VIRTUAL_HUB) {
260
+ const hubData = config.hub ?? null;
261
+ return `export default ${JSON.stringify(hubData)};`;
262
+ }
254
263
  return null;
255
264
  },
256
265
  configureServer(server) {
@@ -2,7 +2,7 @@ import {
2
2
  loadUserConfig,
3
3
  resolveConfig,
4
4
  resolveSections
5
- } from "./chunk-WTT3TFBP.js";
5
+ } from "./chunk-YMRRZEYA.js";
6
6
  import {
7
7
  scanDocs
8
8
  } from "./chunk-6PUBRHS7.js";
@@ -210,6 +210,30 @@ export default defineConfig({
210
210
  } else {
211
211
  console.log(" clearify.config.ts already exists, skipping");
212
212
  }
213
+ if (!options.noClaudeRules) {
214
+ const claudeRulesDir = resolve(cwd, ".claude/rules");
215
+ const claudeRulesPath = resolve(claudeRulesDir, "clearify-docs.md");
216
+ if (!existsSync(claudeRulesPath)) {
217
+ mkdirSync(claudeRulesDir, { recursive: true });
218
+ const hasInternal = !options.noInternal;
219
+ const rulesContent = `# Clearify Documentation Rules
220
+
221
+ When making changes to this project, keep the documentation in sync:
222
+
223
+ 1. **Changelog** \u2014 After any user-facing change, add a bullet under \`## [Unreleased]\` in \`CHANGELOG.md\` using [Keep a Changelog](https://keepachangelog.com/) format (\`Added\`, \`Changed\`, \`Fixed\`, \`Removed\`).
224
+
225
+ 2. **Docs pages** \u2014 When a feature or behavior changes, update the relevant page in \`docs/public/\`. If a new feature has no page, create one.
226
+
227
+ 3. **Config docs** \u2014 When config options are added or changed, update \`docs/public/configuration.md\` with the new option, type, default, and description.
228
+ ${hasInternal ? `
229
+ 4. **Architecture decisions** \u2014 For significant design decisions, add an entry to \`docs/internal/\` explaining the context, decision, and trade-offs.
230
+ ` : ""}`;
231
+ writeFileSync(claudeRulesPath, rulesContent);
232
+ console.log(" Created .claude/rules/clearify-docs.md");
233
+ } else {
234
+ console.log(" .claude/rules/clearify-docs.md already exists, skipping");
235
+ }
236
+ }
213
237
  console.log(`
214
238
  Done! Next steps:
215
239
 
@@ -8,12 +8,12 @@ import {
8
8
  } from "./chunk-SCZZB7OE.js";
9
9
  import {
10
10
  clearifyPlugin
11
- } from "./chunk-HEKR5KM2.js";
11
+ } from "./chunk-3WWPYCGQ.js";
12
12
  import {
13
13
  loadUserConfig,
14
14
  resolveConfig,
15
15
  resolveSections
16
- } from "./chunk-WTT3TFBP.js";
16
+ } from "./chunk-YMRRZEYA.js";
17
17
  import {
18
18
  buildSectionData
19
19
  } from "./chunk-6PUBRHS7.js";
@@ -1,6 +1,6 @@
1
1
  // src/core/config.ts
2
2
  import { z } from "zod";
3
- import { resolve, basename } from "path";
3
+ import { resolve, basename, dirname } from "path";
4
4
  import { pathToFileURL } from "url";
5
5
  import { existsSync, readFileSync, rmSync } from "fs";
6
6
  import { tmpdir } from "os";
@@ -31,6 +31,27 @@ var OpenAPIConfigSchema = z.object({
31
31
  basePath: z.string().default("/api"),
32
32
  generatePages: z.boolean().default(true)
33
33
  }).optional();
34
+ var HubProjectSchema = z.object({
35
+ name: z.string(),
36
+ description: z.string(),
37
+ href: z.string().optional(),
38
+ repo: z.string().optional(),
39
+ status: z.enum(["active", "beta", "planned", "deprecated"]).default("active"),
40
+ icon: z.string().optional(),
41
+ tags: z.array(z.string()).optional()
42
+ });
43
+ var HubProjectPartialSchema = z.object({
44
+ description: z.string(),
45
+ href: z.string().optional(),
46
+ repo: z.string().optional(),
47
+ status: z.enum(["active", "beta", "planned", "deprecated"]).default("active"),
48
+ icon: z.string().optional(),
49
+ tags: z.array(z.string()).optional()
50
+ }).optional();
51
+ var HubConfigSchema = z.object({
52
+ projects: z.array(HubProjectSchema).default([]),
53
+ scan: z.string().optional()
54
+ }).optional();
34
55
  var ClearifyConfigSchema = z.object({
35
56
  name: z.string().default("Documentation"),
36
57
  docsDir: z.string().default("./docs"),
@@ -51,6 +72,8 @@ var ClearifyConfigSchema = z.object({
51
72
  mermaid: MermaidConfigSchema,
52
73
  openapi: OpenAPIConfigSchema,
53
74
  links: z.record(z.string(), z.string()).optional(),
75
+ hub: HubConfigSchema,
76
+ hubProject: HubProjectPartialSchema,
54
77
  customCss: z.string().optional(),
55
78
  headTags: z.array(z.string()).optional()
56
79
  });
@@ -98,7 +121,13 @@ async function loadTsConfig(configPath) {
98
121
  name: "externalize-except-clearify",
99
122
  setup(build2) {
100
123
  build2.onResolve({ filter: /^(clearify|@marlinjai\/clearify)$/ }, () => {
101
- return { path: resolve(import.meta.dirname, "..", "types", "index.js") };
124
+ return { path: "clearify-stub", namespace: "clearify-stub" };
125
+ });
126
+ build2.onLoad({ filter: /.*/, namespace: "clearify-stub" }, () => {
127
+ return {
128
+ contents: "export function defineConfig(config) { return config; }",
129
+ loader: "js"
130
+ };
102
131
  });
103
132
  build2.onResolve({ filter: /.*/ }, (args) => {
104
133
  if (args.path.startsWith(".") || args.path.startsWith("/")) {
@@ -188,10 +217,48 @@ function resolveSections(config, root) {
188
217
  }
189
218
  ];
190
219
  }
220
+ async function scanHubProjects(config, root) {
221
+ if (!config.hub?.scan) return config;
222
+ const { globbySync } = await import("globby");
223
+ const pattern = config.hub.scan;
224
+ const configPaths = globbySync(pattern, { cwd: root, absolute: true });
225
+ const scannedProjects = [];
226
+ for (const configPath of configPaths) {
227
+ try {
228
+ const childConfig = await loadUserConfig(dirname(configPath));
229
+ if (!childConfig.hubProject) continue;
230
+ const name = childConfig.name ?? basename(dirname(configPath));
231
+ const hp = childConfig.hubProject;
232
+ scannedProjects.push({
233
+ name,
234
+ description: hp.description,
235
+ href: hp.href ?? childConfig.siteUrl,
236
+ repo: hp.repo,
237
+ status: hp.status,
238
+ icon: hp.icon,
239
+ tags: hp.tags
240
+ });
241
+ } catch {
242
+ }
243
+ }
244
+ const manualNames = new Set((config.hub.projects ?? []).map((p) => p.name));
245
+ const merged = [
246
+ ...scannedProjects.filter((p) => !manualNames.has(p.name)),
247
+ ...config.hub.projects ?? []
248
+ ];
249
+ return {
250
+ ...config,
251
+ hub: {
252
+ ...config.hub,
253
+ projects: merged
254
+ }
255
+ };
256
+ }
191
257
 
192
258
  export {
193
259
  defaultConfig,
194
260
  loadUserConfig,
195
261
  resolveConfig,
196
- resolveSections
262
+ resolveSections,
263
+ scanHubProjects
197
264
  };
@@ -18,9 +18,9 @@ cli.command("build", "Build static documentation site").action(async () => {
18
18
  const { build } = await import("../node/index.js");
19
19
  await build();
20
20
  });
21
- cli.command("init", "Scaffold a docs folder").option("--no-internal", "Skip creating the internal docs section").action(async (options) => {
21
+ cli.command("init", "Scaffold a docs folder").option("--no-internal", "Skip creating the internal docs section").option("--no-claude-rules", "Skip creating .claude/rules/clearify-docs.md").action(async (options) => {
22
22
  const { init } = await import("../node/index.js");
23
- await init({ noInternal: options.noInternal });
23
+ await init({ noInternal: options.noInternal, noClaudeRules: options.noClaudeRules });
24
24
  });
25
25
  cli.command("check", "Check for broken internal links").action(async () => {
26
26
  const { checkLinks } = await import("../node/check.js");
@@ -37,5 +37,5 @@ cli.command("openapi:generate", "Generate OpenAPI spec from NestJS app").option(
37
37
  });
38
38
  });
39
39
  cli.help();
40
- cli.version("1.5.1");
40
+ cli.version("1.8.0");
41
41
  cli.parse();
@@ -5,5 +5,6 @@ declare const defaultConfig: ClearifyConfig;
5
5
  declare function loadUserConfig(root: string): Promise<Partial<ClearifyConfig>>;
6
6
  declare function resolveConfig(userConfig?: Partial<ClearifyConfig>, root?: string): ClearifyConfig;
7
7
  declare function resolveSections(config: ClearifyConfig, root: string): ResolvedSection[];
8
+ declare function scanHubProjects(config: ClearifyConfig, root: string): Promise<ClearifyConfig>;
8
9
 
9
- export { defaultConfig, loadUserConfig, resolveConfig, resolveSections };
10
+ export { defaultConfig, loadUserConfig, resolveConfig, resolveSections, scanHubProjects };
@@ -2,8 +2,9 @@ import {
2
2
  defaultConfig,
3
3
  loadUserConfig,
4
4
  resolveConfig,
5
- resolveSections
6
- } from "../chunk-WTT3TFBP.js";
5
+ resolveSections,
6
+ scanHubProjects
7
+ } from "../chunk-YMRRZEYA.js";
7
8
  import {
8
9
  defineConfig
9
10
  } from "../chunk-V7LLYIRO.js";
@@ -12,5 +13,6 @@ export {
12
13
  defineConfig,
13
14
  loadUserConfig,
14
15
  resolveConfig,
15
- resolveSections
16
+ resolveSections,
17
+ scanHubProjects
16
18
  };
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  buildSite
3
- } from "../chunk-T4E54OYU.js";
3
+ } from "../chunk-W3MZOFJQ.js";
4
4
  import "../chunk-PRTER35L.js";
5
5
  import "../chunk-SCZZB7OE.js";
6
- import "../chunk-HEKR5KM2.js";
6
+ import "../chunk-3WWPYCGQ.js";
7
7
  import "../chunk-B2Q23JW3.js";
8
- import "../chunk-WTT3TFBP.js";
8
+ import "../chunk-YMRRZEYA.js";
9
9
  import "../chunk-V7LLYIRO.js";
10
10
  import "../chunk-6PUBRHS7.js";
11
11
  import "../chunk-5TD7NQIW.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  checkLinks
3
- } from "../chunk-DTMKLAIC.js";
4
- import "../chunk-WTT3TFBP.js";
3
+ } from "../chunk-4BOXMNVR.js";
4
+ import "../chunk-YMRRZEYA.js";
5
5
  import "../chunk-V7LLYIRO.js";
6
6
  import "../chunk-6PUBRHS7.js";
7
7
  import "../chunk-5TD7NQIW.js";
@@ -1,24 +1,24 @@
1
1
  import {
2
2
  buildSite
3
- } from "../chunk-T4E54OYU.js";
3
+ } from "../chunk-W3MZOFJQ.js";
4
4
  import {
5
5
  remarkMermaidToComponent
6
6
  } from "../chunk-PRTER35L.js";
7
7
  import "../chunk-SCZZB7OE.js";
8
8
  import {
9
9
  clearifyPlugin
10
- } from "../chunk-HEKR5KM2.js";
10
+ } from "../chunk-3WWPYCGQ.js";
11
11
  import "../chunk-B2Q23JW3.js";
12
12
  import {
13
13
  init
14
- } from "../chunk-MWUXPDQK.js";
14
+ } from "../chunk-QIAMD2Q6.js";
15
15
  import {
16
16
  checkLinks
17
- } from "../chunk-DTMKLAIC.js";
17
+ } from "../chunk-4BOXMNVR.js";
18
18
  import {
19
19
  loadUserConfig,
20
20
  resolveConfig
21
- } from "../chunk-WTT3TFBP.js";
21
+ } from "../chunk-YMRRZEYA.js";
22
22
  import "../chunk-V7LLYIRO.js";
23
23
  import "../chunk-6PUBRHS7.js";
24
24
  import "../chunk-5TD7NQIW.js";
@@ -1,5 +1,6 @@
1
1
  interface InitOptions {
2
2
  noInternal?: boolean;
3
+ noClaudeRules?: boolean;
3
4
  }
4
5
  declare function init(options?: InitOptions): Promise<void>;
5
6
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  init
3
- } from "../chunk-MWUXPDQK.js";
3
+ } from "../chunk-QIAMD2Q6.js";
4
4
  export {
5
5
  init
6
6
  };
@@ -50,6 +50,8 @@ interface ClearifyConfig {
50
50
  github?: string;
51
51
  [key: string]: string | undefined;
52
52
  };
53
+ hub?: HubConfig;
54
+ hubProject?: Omit<HubProject, 'name'>;
53
55
  customCss?: string;
54
56
  headTags?: string[];
55
57
  }
@@ -75,6 +77,19 @@ interface RouteEntry {
75
77
  componentPath?: string;
76
78
  redirectTo?: string;
77
79
  }
80
+ interface HubProject {
81
+ name: string;
82
+ description: string;
83
+ href?: string;
84
+ repo?: string;
85
+ status?: 'active' | 'beta' | 'planned' | 'deprecated';
86
+ icon?: string;
87
+ tags?: string[];
88
+ }
89
+ interface HubConfig {
90
+ projects: HubProject[];
91
+ scan?: string;
92
+ }
78
93
  declare function defineConfig(config: Partial<ClearifyConfig>): Partial<ClearifyConfig>;
79
94
 
80
- export { type ClearifyConfig, type NavigationItem, type PageFrontmatter, type ResolvedSection, type RouteEntry, type SectionConfig, type SectionNavigation, defineConfig };
95
+ export { type ClearifyConfig, type HubConfig, type HubProject, type NavigationItem, type PageFrontmatter, type ResolvedSection, type RouteEntry, type SectionConfig, type SectionNavigation, defineConfig };
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  clearifyPlugin
3
- } from "../chunk-HEKR5KM2.js";
3
+ } from "../chunk-3WWPYCGQ.js";
4
4
  import "../chunk-B2Q23JW3.js";
5
- import "../chunk-WTT3TFBP.js";
5
+ import "../chunk-YMRRZEYA.js";
6
6
  import "../chunk-V7LLYIRO.js";
7
7
  import "../chunk-6PUBRHS7.js";
8
8
  import "../chunk-5TD7NQIW.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marlinjai/clearify",
3
- "version": "1.7.2",
3
+ "version": "1.8.0",
4
4
  "description": "An open-source documentation site generator. Turn markdown into beautiful docs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,10 +22,6 @@
22
22
  "src/client",
23
23
  "src/theme"
24
24
  ],
25
- "scripts": {
26
- "build": "tsup",
27
- "dev:self": "node bin/clearify.js dev"
28
- },
29
25
  "dependencies": {
30
26
  "@mdx-js/react": "^3.1.1",
31
27
  "@mdx-js/rollup": "^3.1.1",
@@ -92,5 +88,9 @@
92
88
  "react",
93
89
  "mermaid",
94
90
  "openapi"
95
- ]
96
- }
91
+ ],
92
+ "scripts": {
93
+ "build": "tsup",
94
+ "dev:self": "node bin/clearify.js dev"
95
+ }
96
+ }
@@ -13,6 +13,9 @@ import { Badge } from './components/Badge.js';
13
13
  import { Tooltip } from './components/Tooltip.js';
14
14
  import { Columns, Column } from './components/Columns.js';
15
15
  import { Frame } from './components/Frame.js';
16
+ import { StatusBadge } from './components/StatusBadge.js';
17
+ import { ProjectCard } from './components/ProjectCard.js';
18
+ import { ProjectGrid } from './components/ProjectGrid.js';
16
19
  import { CodeBlock } from './CodeBlock.js';
17
20
 
18
21
  export const mdxComponents = {
@@ -35,6 +38,9 @@ export const mdxComponents = {
35
38
  Columns,
36
39
  Column,
37
40
  Frame,
41
+ StatusBadge,
42
+ ProjectCard,
43
+ ProjectGrid,
38
44
  // Override default pre element with CodeBlock
39
45
  pre: (props: React.HTMLAttributes<HTMLPreElement>) => <CodeBlock {...props} />,
40
46
  };
@@ -0,0 +1,134 @@
1
+ import React from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { StatusBadge } from './StatusBadge.js';
4
+
5
+ interface ProjectCardProps {
6
+ name: string;
7
+ description: string;
8
+ href?: string;
9
+ repo?: string;
10
+ status?: 'active' | 'beta' | 'planned' | 'deprecated';
11
+ icon?: string;
12
+ tags?: string[];
13
+ children?: React.ReactNode;
14
+ }
15
+
16
+ export function ProjectCard({ name, description, href, repo, status, icon, tags, children }: ProjectCardProps) {
17
+ const content = (
18
+ <div
19
+ style={{
20
+ border: '1px solid var(--clearify-border)',
21
+ borderRadius: 'var(--clearify-radius-lg)',
22
+ padding: '1.375rem',
23
+ transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
24
+ cursor: href ? 'pointer' : 'default',
25
+ height: '100%',
26
+ background: 'var(--clearify-bg)',
27
+ position: 'relative',
28
+ overflow: 'hidden',
29
+ display: 'flex',
30
+ flexDirection: 'column' as const,
31
+ }}
32
+ className="clearify-project-card"
33
+ >
34
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '0.625rem' }}>
35
+ {icon && (
36
+ <div
37
+ style={{
38
+ fontSize: '1.25rem',
39
+ width: 36,
40
+ height: 36,
41
+ display: 'flex',
42
+ alignItems: 'center',
43
+ justifyContent: 'center',
44
+ borderRadius: 'var(--clearify-radius-sm)',
45
+ background: 'var(--clearify-gradient-subtle)',
46
+ border: '1px solid var(--clearify-border)',
47
+ flexShrink: 0,
48
+ }}
49
+ >
50
+ {icon}
51
+ </div>
52
+ )}
53
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
54
+ {status && <StatusBadge status={status} />}
55
+ {repo && (
56
+ <a
57
+ href={repo}
58
+ target="_blank"
59
+ rel="noopener noreferrer"
60
+ onClick={(e) => e.stopPropagation()}
61
+ style={{
62
+ color: 'var(--clearify-text-secondary)',
63
+ display: 'inline-flex',
64
+ alignItems: 'center',
65
+ transition: 'color 0.15s',
66
+ }}
67
+ className="clearify-project-card-repo"
68
+ title="View repository"
69
+ >
70
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
71
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
72
+ </svg>
73
+ </a>
74
+ )}
75
+ </div>
76
+ </div>
77
+
78
+ <div style={{ fontWeight: 600, marginBottom: '0.25rem', fontSize: '0.9375rem', letterSpacing: '-0.01em' }}>
79
+ {name}
80
+ </div>
81
+ <div style={{ fontSize: '0.8125rem', color: 'var(--clearify-text-secondary)', lineHeight: 1.55, flex: 1 }}>
82
+ {description}
83
+ </div>
84
+
85
+ {children && <div style={{ marginTop: '0.625rem', fontSize: '0.8125rem' }}>{children}</div>}
86
+
87
+ {tags && tags.length > 0 && (
88
+ <div style={{ display: 'flex', flexWrap: 'wrap' as const, gap: '0.375rem', marginTop: '0.75rem' }}>
89
+ {tags.map((tag) => (
90
+ <span
91
+ key={tag}
92
+ style={{
93
+ display: 'inline-block',
94
+ padding: '0.0625rem 0.4375rem',
95
+ borderRadius: '9999px',
96
+ fontSize: '0.6875rem',
97
+ fontWeight: 500,
98
+ background: 'rgba(107, 114, 128, 0.08)',
99
+ color: 'var(--clearify-text-secondary)',
100
+ fontFamily: 'var(--font-sans)',
101
+ }}
102
+ className="clearify-project-card-tag"
103
+ >
104
+ {tag}
105
+ </span>
106
+ ))}
107
+ </div>
108
+ )}
109
+
110
+ <style>{`
111
+ .clearify-project-card:hover {
112
+ border-color: var(--clearify-border-strong) !important;
113
+ box-shadow: var(--clearify-shadow) !important;
114
+ transform: translateY(-1px);
115
+ }
116
+ .clearify-project-card-repo:hover {
117
+ color: var(--clearify-text) !important;
118
+ }
119
+ .dark .clearify-project-card-tag {
120
+ background: rgba(107, 114, 128, 0.15) !important;
121
+ }
122
+ `}</style>
123
+ </div>
124
+ );
125
+
126
+ if (href) {
127
+ if (href.startsWith('/')) {
128
+ return <Link to={href} style={{ textDecoration: 'none', color: 'inherit' }}>{content}</Link>;
129
+ }
130
+ return <a href={href} target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none', color: 'inherit' }}>{content}</a>;
131
+ }
132
+
133
+ return content;
134
+ }
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import hub from 'virtual:clearify/hub';
3
+ import { ProjectCard } from './ProjectCard.js';
4
+
5
+ interface ProjectGridProps {
6
+ cols?: number;
7
+ children?: React.ReactNode;
8
+ }
9
+
10
+ export function ProjectGrid({ cols = 3, children }: ProjectGridProps) {
11
+ const hasChildren = React.Children.count(children) > 0;
12
+
13
+ return (
14
+ <div
15
+ style={{
16
+ display: 'grid',
17
+ gridTemplateColumns: `repeat(${cols}, 1fr)`,
18
+ gap: '0.875rem',
19
+ marginBottom: '1.5rem',
20
+ }}
21
+ className="clearify-project-grid"
22
+ >
23
+ {hasChildren
24
+ ? children
25
+ : hub?.projects.map((project) => (
26
+ <ProjectCard
27
+ key={project.name}
28
+ name={project.name}
29
+ description={project.description}
30
+ href={project.href}
31
+ repo={project.repo}
32
+ status={project.status}
33
+ icon={project.icon}
34
+ tags={project.tags}
35
+ />
36
+ ))}
37
+
38
+ <style>{`
39
+ @media (max-width: 900px) {
40
+ .clearify-project-grid {
41
+ grid-template-columns: repeat(2, 1fr) !important;
42
+ }
43
+ }
44
+ @media (max-width: 640px) {
45
+ .clearify-project-grid {
46
+ grid-template-columns: 1fr !important;
47
+ }
48
+ }
49
+ `}</style>
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+
3
+ const statusStyles = {
4
+ active: {
5
+ bg: 'rgba(34, 197, 94, 0.08)',
6
+ bgDark: 'rgba(34, 197, 94, 0.15)',
7
+ color: '#16a34a',
8
+ colorDark: '#4ade80',
9
+ label: 'Active',
10
+ },
11
+ beta: {
12
+ bg: 'rgba(99, 102, 241, 0.08)',
13
+ bgDark: 'rgba(99, 102, 241, 0.15)',
14
+ color: '#6366f1',
15
+ colorDark: '#818cf8',
16
+ label: 'Beta',
17
+ },
18
+ planned: {
19
+ bg: 'rgba(245, 158, 11, 0.08)',
20
+ bgDark: 'rgba(245, 158, 11, 0.15)',
21
+ color: '#d97706',
22
+ colorDark: '#fbbf24',
23
+ label: 'Planned',
24
+ },
25
+ deprecated: {
26
+ bg: 'rgba(239, 68, 68, 0.08)',
27
+ bgDark: 'rgba(239, 68, 68, 0.15)',
28
+ color: '#dc2626',
29
+ colorDark: '#f87171',
30
+ label: 'Deprecated',
31
+ },
32
+ };
33
+
34
+ interface StatusBadgeProps {
35
+ status: 'active' | 'beta' | 'planned' | 'deprecated';
36
+ }
37
+
38
+ export function StatusBadge({ status }: StatusBadgeProps) {
39
+ const s = statusStyles[status];
40
+ const badgeClass = `clearify-status-badge-${status}`;
41
+
42
+ return (
43
+ <span
44
+ style={{
45
+ display: 'inline-flex',
46
+ alignItems: 'center',
47
+ padding: '0.125rem 0.5rem',
48
+ borderRadius: '9999px',
49
+ fontSize: '0.6875rem',
50
+ fontWeight: 500,
51
+ lineHeight: 1.5,
52
+ backgroundColor: s.bg,
53
+ color: s.color,
54
+ fontFamily: 'var(--font-sans)',
55
+ whiteSpace: 'nowrap',
56
+ letterSpacing: '0.01em',
57
+ }}
58
+ className={badgeClass}
59
+ >
60
+ {s.label}
61
+
62
+ <style>{`
63
+ .dark .${badgeClass} {
64
+ background-color: ${s.bgDark} !important;
65
+ color: ${s.colorDark} !important;
66
+ }
67
+ `}</style>
68
+ </span>
69
+ );
70
+ }
@@ -12,3 +12,6 @@ export { Badge } from './Badge.js';
12
12
  export { Tooltip } from './Tooltip.js';
13
13
  export { Columns, Column } from './Columns.js';
14
14
  export { Frame } from './Frame.js';
15
+ export { StatusBadge } from './StatusBadge.js';
16
+ export { ProjectCard } from './ProjectCard.js';
17
+ export { ProjectGrid } from './ProjectGrid.js';