@grove-dev/astro 0.1.5 → 0.2.5

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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +4 -25
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +56 -22
  5. package/dist/index.js.map +1 -1
  6. package/dist/layouts/index.d.ts +25 -0
  7. package/dist/layouts/index.d.ts.map +1 -0
  8. package/dist/layouts/index.js +25 -0
  9. package/dist/layouts/index.js.map +1 -0
  10. package/dist/lib/display.d.ts +33 -0
  11. package/dist/lib/display.d.ts.map +1 -0
  12. package/dist/lib/display.js +104 -0
  13. package/dist/lib/display.js.map +1 -0
  14. package/dist/lib/format.d.ts +30 -0
  15. package/dist/lib/format.d.ts.map +1 -0
  16. package/dist/lib/format.js +73 -0
  17. package/dist/lib/format.js.map +1 -0
  18. package/dist/lib/index.d.ts +26 -0
  19. package/dist/lib/index.d.ts.map +1 -0
  20. package/dist/lib/index.js +26 -0
  21. package/dist/lib/index.js.map +1 -0
  22. package/dist/lib/lenses.d.ts +96 -0
  23. package/dist/lib/lenses.d.ts.map +1 -0
  24. package/dist/lib/lenses.js +181 -0
  25. package/dist/lib/lenses.js.map +1 -0
  26. package/dist/lib/repo.d.ts +30 -0
  27. package/dist/lib/repo.d.ts.map +1 -0
  28. package/dist/lib/repo.js +44 -0
  29. package/dist/lib/repo.js.map +1 -0
  30. package/dist/lib/scores.d.ts +38 -0
  31. package/dist/lib/scores.d.ts.map +1 -0
  32. package/dist/lib/scores.js +83 -0
  33. package/dist/lib/scores.js.map +1 -0
  34. package/dist/lib/search.d.ts +199 -0
  35. package/dist/lib/search.d.ts.map +1 -0
  36. package/dist/lib/search.js +357 -0
  37. package/dist/lib/search.js.map +1 -0
  38. package/dist/lib/taxonomy-counts.d.ts +25 -0
  39. package/dist/lib/taxonomy-counts.d.ts.map +1 -0
  40. package/dist/lib/taxonomy-counts.js +41 -0
  41. package/dist/lib/taxonomy-counts.js.map +1 -0
  42. package/package.json +7 -7
  43. package/src/components/AppsIndexRow.astro +204 -0
  44. package/src/components/AppsPagination.astro +126 -0
  45. package/src/components/CategoryGrid.astro +86 -17
  46. package/src/components/ContributorsGrid.astro +149 -0
  47. package/src/components/DecisionRow.astro +89 -0
  48. package/src/components/ExploreByCategory.astro +67 -0
  49. package/src/components/ExploreByStack.astro +72 -0
  50. package/src/components/Hero.astro +246 -0
  51. package/src/components/Icon.astro +223 -0
  52. package/src/components/ItemCard.astro +306 -30
  53. package/src/components/MinimalAbout.astro +82 -0
  54. package/src/components/OriginalCollection.astro +116 -0
  55. package/src/components/RecordSection.astro +88 -0
  56. package/src/components/RefinePanel.astro +197 -0
  57. package/src/components/ScoreBars.astro +92 -22
  58. package/src/components/SmartLensTabs.astro +71 -0
  59. package/src/components/StackGrid.astro +132 -0
  60. package/src/components/WhyThisExists.astro +116 -0
  61. package/src/index.ts +61 -14
  62. package/src/layouts/BaseLayout.astro +227 -15
  63. package/src/layouts/Container.astro +57 -0
  64. package/src/layouts/Footer.astro +235 -0
  65. package/src/layouts/Header.astro +159 -0
  66. package/src/layouts/SectionHeader.astro +58 -0
  67. package/{templates/default/src/components/layout → src/layouts}/Seo.astro +62 -38
  68. package/src/layouts/ThemeToggle.astro +110 -0
  69. package/src/layouts/astro-shim.d.ts +29 -0
  70. package/src/layouts/index.ts +24 -0
  71. package/src/lib/display.ts +118 -0
  72. package/src/lib/format.ts +65 -0
  73. package/src/lib/index.ts +26 -0
  74. package/src/lib/lenses.ts +241 -0
  75. package/src/lib/repo.ts +44 -0
  76. package/src/lib/scores.ts +94 -0
  77. package/src/lib/search.ts +481 -0
  78. package/src/lib/taxonomy-counts.ts +60 -0
  79. package/src/styles.css +366 -638
  80. package/templates/default/astro.config.mjs +8 -6
  81. package/templates/default/content/pages/about.md +66 -0
  82. package/templates/default/content/pages/methodology.md +103 -0
  83. package/templates/default/data/generated/records.full.json +8 -0
  84. package/templates/default/data/generated/records.index.json +7 -0
  85. package/templates/default/data/generated/records.json +8 -0
  86. package/templates/default/data/generated/site-config.json +31 -0
  87. package/templates/default/data/records/.gitkeep +0 -0
  88. package/templates/default/grove.config.ts +36 -0
  89. package/templates/default/package.json +6 -5
  90. package/templates/default/public/llms-full.txt +14 -0
  91. package/templates/default/public/llms.txt +11 -0
  92. package/templates/default/src/components/layout/BaseLayout.astro +10 -57
  93. package/templates/default/src/components/layout/Footer.astro +16 -134
  94. package/templates/default/src/components/layout/Header.astro +25 -126
  95. package/templates/default/src/data/records.ts +193 -0
  96. package/templates/default/src/lib/markdown.ts +171 -0
  97. package/templates/default/src/pages/about.astro +95 -429
  98. package/templates/default/src/pages/index.astro +263 -61
  99. package/templates/default/src/pages/projects/[slug].astro +480 -0
  100. package/templates/default/src/pages/projects/index.astro +193 -0
  101. package/templates/default/src/pages/sitemap.xml.ts +73 -61
  102. package/templates/default/src/pages/submit.astro +122 -411
  103. package/templates/default/src/styles/global.css +42 -68
  104. package/src/components/ActiveFilterChips.astro +0 -20
  105. package/src/components/DirectoryFilters.astro +0 -100
  106. package/src/components/DirectoryHero.astro +0 -32
  107. package/src/components/HealthBadge.astro +0 -12
  108. package/src/components/LensTabs.astro +0 -26
  109. package/src/components/MethodologyPanel.astro +0 -20
  110. package/src/components/Pagination.astro +0 -24
  111. package/src/components/ProjectDetail.astro +0 -166
  112. package/src/components/SubmitDraft.astro +0 -137
  113. package/templates/default/src/components/AppCard.astro +0 -248
  114. package/templates/default/src/components/AppSection.astro +0 -58
  115. package/templates/default/src/components/AppsIndexRow.astro +0 -230
  116. package/templates/default/src/components/AppsPagination.astro +0 -147
  117. package/templates/default/src/components/CategoryGrid.astro +0 -80
  118. package/templates/default/src/components/ContributorsGrid.astro +0 -105
  119. package/templates/default/src/components/DecisionRow.astro +0 -219
  120. package/templates/default/src/components/ExploreByCategory.astro +0 -74
  121. package/templates/default/src/components/ExploreByStack.astro +0 -102
  122. package/templates/default/src/components/Hero.astro +0 -176
  123. package/templates/default/src/components/MinimalAbout.astro +0 -57
  124. package/templates/default/src/components/OriginalCollection.astro +0 -144
  125. package/templates/default/src/components/RefinePanel.astro +0 -272
  126. package/templates/default/src/components/ScoreBars.astro +0 -72
  127. package/templates/default/src/components/SmartLensTabs.astro +0 -65
  128. package/templates/default/src/components/StackGrid.astro +0 -95
  129. package/templates/default/src/components/WhyThisExists.astro +0 -81
  130. package/templates/default/src/components/icons/Icon.astro +0 -148
  131. package/templates/default/src/components/layout/SectionHeader.astro +0 -42
  132. package/templates/default/src/components/layout/ThemeToggle.astro +0 -97
  133. package/templates/default/src/data/apps.ts +0 -149
  134. package/templates/default/src/data/categories.ts +0 -27
  135. package/templates/default/src/data/config.ts +0 -28
  136. package/templates/default/src/data/contributors.ts +0 -96
  137. package/templates/default/src/data/stacks.ts +0 -47
  138. package/templates/default/src/data/stats.ts +0 -65
  139. package/templates/default/src/data/types.ts +0 -248
  140. package/templates/default/src/lib/apps-search.ts +0 -228
  141. package/templates/default/src/lib/format.ts +0 -36
  142. package/templates/default/src/lib/lenses.ts +0 -98
  143. package/templates/default/src/lib/repo.ts +0 -12
  144. package/templates/default/src/lib/scores.ts +0 -51
  145. package/templates/default/src/lib/taxonomy-counts.ts +0 -31
  146. package/templates/default/src/pages/apps/[slug].astro +0 -709
  147. package/templates/default/src/pages/apps/index.astro +0 -669
  148. package/templates/default/src/pages/contributors.astro +0 -142
@@ -1,20 +0,0 @@
1
- ---
2
- import type { DirectoryFilters } from "@grove-dev/ui";
3
- import { activeFilterChips } from "@grove-dev/ui";
4
-
5
- interface Props {
6
- filters: DirectoryFilters;
7
- }
8
-
9
- const { filters } = Astro.props;
10
- const chips = activeFilterChips(filters);
11
- ---
12
-
13
- {chips.length > 0 && (
14
- <div class="oc-active-chips" aria-label="Active filters">
15
- {chips.map((chip) => (
16
- <span data-filter-chip={chip.key}>{chip.label}</span>
17
- ))}
18
- <a href="/projects">Clear all</a>
19
- </div>
20
- )}
@@ -1,100 +0,0 @@
1
- ---
2
- import type { HealthStatus } from "@grove-dev/core";
3
-
4
- interface Props {
5
- q?: string;
6
- category?: string;
7
- tag?: string;
8
- language?: string;
9
- license?: string;
10
- health?: string;
11
- maintained?: boolean;
12
- hideArchived?: boolean;
13
- hasRecentRelease?: boolean;
14
- categories: string[];
15
- tags: string[];
16
- languages: string[];
17
- licenses: string[];
18
- action?: string;
19
- }
20
-
21
- const {
22
- q = "",
23
- category = "",
24
- tag = "",
25
- language = "",
26
- license = "",
27
- health = "all",
28
- maintained = false,
29
- hideArchived = false,
30
- hasRecentRelease = false,
31
- categories,
32
- tags,
33
- languages,
34
- licenses,
35
- action = "/projects",
36
- } = Astro.props;
37
-
38
- const healthOptions: Array<HealthStatus | "all"> = [
39
- "all",
40
- "mature",
41
- "active",
42
- "stale",
43
- "needs_review",
44
- "inactive",
45
- "archived",
46
- "unknown",
47
- ];
48
- ---
49
-
50
- <form class="oc-filters" action={action} method="get" role="search">
51
- <label class="oc-filter-search">
52
- <span>Search</span>
53
- <input type="search" name="q" value={q} placeholder="Name, description, tag, license..." />
54
- </label>
55
- <label>
56
- <span>Category</span>
57
- <select name="category">
58
- <option value="">All categories</option>
59
- {categories.map((value) => <option value={value} selected={value === category}>{value}</option>)}
60
- </select>
61
- </label>
62
- <label>
63
- <span>Health</span>
64
- <select name="health">
65
- {healthOptions.map((value) => (
66
- <option value={value} selected={value === health}>{value.replace(/_/g, " ")}</option>
67
- ))}
68
- </select>
69
- </label>
70
- <label>
71
- <span>Tag</span>
72
- <select name="tag">
73
- <option value="">All tags</option>
74
- {tags.map((value) => <option value={value} selected={value === tag}>{value}</option>)}
75
- </select>
76
- </label>
77
- <label>
78
- <span>Language</span>
79
- <select name="language">
80
- <option value="">All languages</option>
81
- {languages.map((value) => <option value={value} selected={value === language}>{value}</option>)}
82
- </select>
83
- </label>
84
- <label>
85
- <span>License</span>
86
- <select name="license">
87
- <option value="">All licenses</option>
88
- {licenses.map((value) => <option value={value} selected={value === license}>{value}</option>)}
89
- </select>
90
- </label>
91
- <div class="oc-filter-toggles">
92
- <label><input type="checkbox" name="maintained" value="1" checked={maintained} /> Maintained only</label>
93
- <label><input type="checkbox" name="hideArchived" value="1" checked={hideArchived} /> Hide archived</label>
94
- <label><input type="checkbox" name="hasRecentRelease" value="1" checked={hasRecentRelease} /> Recent release</label>
95
- </div>
96
- <div class="oc-filter-actions">
97
- <button type="submit">Apply</button>
98
- <a href={action}>Reset</a>
99
- </div>
100
- </form>
@@ -1,32 +0,0 @@
1
- ---
2
- import type { DirectoryStats } from "@grove-dev/ui";
3
-
4
- interface Props {
5
- name: string;
6
- tagline: string;
7
- stats: DirectoryStats;
8
- }
9
-
10
- const { name, tagline, stats } = Astro.props;
11
- ---
12
-
13
- <section class="oc-hero">
14
- <div>
15
- <p class="oc-eyebrow">Grove</p>
16
- <h1>{name}</h1>
17
- <p class="oc-lede">{tagline}</p>
18
- </div>
19
- <form class="oc-hero-search" action="/projects" role="search">
20
- <label>
21
- <span class="oc-sr-only">Search projects</span>
22
- <input type="search" name="q" placeholder="Search projects, tags, licenses..." />
23
- </label>
24
- <button type="submit">Search</button>
25
- </form>
26
- <dl class="oc-stats">
27
- <div><dt>Projects</dt><dd>{stats.total}</dd></div>
28
- <div><dt>Active</dt><dd>{stats.active}</dd></div>
29
- <div><dt>Mature</dt><dd>{stats.mature}</dd></div>
30
- <div><dt>Needs review</dt><dd>{stats.needsReview}</dd></div>
31
- </dl>
32
- </section>
@@ -1,12 +0,0 @@
1
- ---
2
- import type { HealthStatus } from "@grove-dev/core";
3
-
4
- interface Props {
5
- status?: HealthStatus;
6
- }
7
-
8
- const { status = "unknown" } = Astro.props;
9
- const label = status.replace(/_/g, " ");
10
- ---
11
-
12
- <span class={`oc-health oc-health-${status}`}>{label}</span>
@@ -1,26 +0,0 @@
1
- ---
2
- import { LENSES } from "@grove-dev/ui";
3
-
4
- interface Props {
5
- current?: string;
6
- action?: string;
7
- }
8
-
9
- const { current = "all", action = "/projects" } = Astro.props;
10
- ---
11
-
12
- <nav class="oc-lenses" aria-label="Curated lenses">
13
- {LENSES.map((lens) => {
14
- const params = new URLSearchParams();
15
- for (const [key, value] of Object.entries(lens.params)) {
16
- params.set(key, String(value));
17
- }
18
- const href = lens.id === "all" ? action : `${action}?${params.toString()}`;
19
- const active = current === lens.id || (current === "all" && lens.id === "all");
20
- return (
21
- <a href={href} data-lens-id={lens.id} aria-current={active ? "page" : undefined} title={lens.description}>
22
- {lens.label}
23
- </a>
24
- );
25
- })}
26
- </nav>
@@ -1,20 +0,0 @@
1
- ---
2
- ---
3
-
4
- <section class="oc-methodology">
5
- <div>
6
- <p class="oc-eyebrow">Methodology</p>
7
- <h2>Health is a signal, not an automatic deletion rule.</h2>
8
- <p>
9
- Grove checks repository activity, archived state, releases, license,
10
- adoption, and metadata freshness. Projects can still be kept, highlighted,
11
- hidden, or preserved as historical through human curation decisions.
12
- </p>
13
- </div>
14
- <ul>
15
- <li>Archived repositories are flagged clearly.</li>
16
- <li>Recent commits and releases increase confidence.</li>
17
- <li>Missing metadata becomes unknown or needs review.</li>
18
- <li>Humans control final visibility in decisions.yml.</li>
19
- </ul>
20
- </section>
@@ -1,24 +0,0 @@
1
- ---
2
- interface Props {
3
- page: number;
4
- pages: number;
5
- base?: string;
6
- }
7
-
8
- const { page, pages, base = "/projects" } = Astro.props;
9
-
10
- function href(target: number) {
11
- const current = new URL(Astro.url);
12
- current.searchParams.set("page", String(target));
13
- const query = current.searchParams.toString();
14
- return `${base}${query ? `?${query}` : ""}`;
15
- }
16
- ---
17
-
18
- {pages > 1 && (
19
- <nav class="oc-pagination" aria-label="Pagination">
20
- <a href={href(Math.max(1, page - 1))} aria-disabled={page <= 1 ? "true" : undefined}>Previous</a>
21
- <span>Page {page} of {pages}</span>
22
- <a href={href(Math.min(pages, page + 1))} aria-disabled={page >= pages ? "true" : undefined}>Next</a>
23
- </nav>
24
- )}
@@ -1,166 +0,0 @@
1
- ---
2
- import type { CuratedItem, HealthEntry, Decision } from "@grove-dev/core";
3
- import HealthBadge from "./HealthBadge.astro";
4
- import ScoreBars from "./ScoreBars.astro";
5
- import { SCORE_LABELS, SCORE_REASONING } from "@grove-dev/ui";
6
-
7
- interface Props {
8
- item: CuratedItem;
9
- health?: HealthEntry;
10
- decision?: Decision;
11
- }
12
-
13
- const { item, health, decision } = Astro.props;
14
- const links = Object.entries(item.links).filter(([, href]) => Boolean(href));
15
- const channels = item.distribution.channels ?? [];
16
- const languages = health?.github?.languages
17
- ? Object.entries(health.github.languages).sort((a, b) => b[1] - a[1]).slice(0, 5)
18
- : [];
19
- const totalLanguageBytes = Math.max(1, languages.reduce((sum, [, bytes]) => sum + bytes, 0));
20
- const files = health?.github?.files ?? {};
21
- const fileSignals = [
22
- ["README", files.readme],
23
- ["Contributing", files.contributing],
24
- ["Security", files.security],
25
- ["Issue templates", files.issueTemplates],
26
- ["PR template", files.pullRequestTemplate],
27
- ].filter(([, ok]) => ok !== undefined);
28
- const monthly = health?.github?.monthlyCommits ?? [];
29
- const maxCommits = Math.max(1, ...monthly.map((entry) => entry.commits));
30
- ---
31
-
32
- <article class="oc-detail">
33
- <header class="oc-detail-header">
34
- <div>
35
- <p class="oc-eyebrow">{item.taxonomy.category}</p>
36
- <h1>{item.name}</h1>
37
- <p class="oc-lede">{item.description}</p>
38
- </div>
39
- <HealthBadge status={health?.health.status} />
40
- </header>
41
-
42
- <div class="oc-detail-grid">
43
- <section class="oc-detail-wide">
44
- <h2>Curation Scores</h2>
45
- <ScoreBars scores={item.curation.scores} />
46
- <dl class="oc-score-reasons">
47
- {(Object.keys(SCORE_LABELS) as Array<keyof typeof SCORE_LABELS>).map((key) => (
48
- <div>
49
- <dt>{SCORE_LABELS[key]}</dt>
50
- <dd>{SCORE_REASONING[key]}</dd>
51
- </div>
52
- ))}
53
- </dl>
54
- </section>
55
- <section>
56
- <h2>Links</h2>
57
- <ul class="oc-link-list">
58
- {links.map(([label, href]) => <li><a href={href}>{label}</a></li>)}
59
- </ul>
60
- {channels.length > 0 && (
61
- <>
62
- <h3>Distribution</h3>
63
- <ul class="oc-link-list">
64
- {channels.map((channel) => (
65
- <li><a href={channel.url}>{channel.label ?? channel.type}</a>{channel.platform ? ` · ${channel.platform}` : ""}</li>
66
- ))}
67
- </ul>
68
- </>
69
- )}
70
- </section>
71
- <section>
72
- <h2>Metadata</h2>
73
- <dl class="oc-fact-list">
74
- <div><dt>Stars</dt><dd>{health?.github?.stars?.toLocaleString() ?? "Unknown"}</dd></div>
75
- <div><dt>Forks</dt><dd>{health?.github?.forks?.toLocaleString() ?? "Unknown"}</dd></div>
76
- <div><dt>License</dt><dd>{health?.github?.license ?? "Unknown"}</dd></div>
77
- <div><dt>Language</dt><dd>{item.taxonomy.language ?? health?.github?.language ?? "Unknown"}</dd></div>
78
- <div><dt>Last activity</dt><dd>{health?.github?.pushedAt?.slice(0, 10) ?? "Unknown"}</dd></div>
79
- <div><dt>Latest release</dt><dd>{health?.github?.latestReleaseAt?.slice(0, 10) ?? "Unknown"}</dd></div>
80
- <div><dt>Contributors</dt><dd>{health?.github?.contributorsKnown?.toLocaleString() ?? "Unknown"}</dd></div>
81
- <div><dt>Open PRs</dt><dd>{health?.github?.openPullRequests?.toLocaleString() ?? "Unknown"}</dd></div>
82
- </dl>
83
- </section>
84
- <section>
85
- <h2>Health Signals</h2>
86
- <ul class="oc-signal-list">
87
- {(health?.health.reasons ?? ["No health metadata available"]).map((reason) => <li>{reason}</li>)}
88
- </ul>
89
- </section>
90
- <section>
91
- <h2>Curation</h2>
92
- {decision ? (
93
- <p><strong>{decision.decision.visibility.replace(/_/g, " ")}</strong>: {decision.decision.reason}</p>
94
- ) : (
95
- <p>No manual curation decision has been recorded yet.</p>
96
- )}
97
- {item.curation.reviewed !== undefined && (
98
- <p>{item.curation.reviewed ? "Curator reviewed" : "Generated curation pending review"}{item.curation.reviewedBy ? ` by ${item.curation.reviewedBy}` : ""}.</p>
99
- )}
100
- {item.source.file && <p class="oc-source">Source: {item.source.file}{item.source.line ? `:${item.source.line}` : ""}</p>}
101
- </section>
102
- {(item.curation.bestFor.length > 0 || item.curation.whyListed.length > 0 || item.curation.caveats.length > 0) && (
103
- <section class="oc-detail-wide">
104
- <h2>Why Included</h2>
105
- <div class="oc-curation-columns">
106
- {item.curation.bestFor.length > 0 && (
107
- <div>
108
- <h3>Best for</h3>
109
- <ul class="oc-signal-list">{item.curation.bestFor.map((value) => <li>{value}</li>)}</ul>
110
- </div>
111
- )}
112
- {item.curation.whyListed.length > 0 && (
113
- <div>
114
- <h3>Why listed</h3>
115
- <ul class="oc-signal-list">{item.curation.whyListed.map((value) => <li>{value}</li>)}</ul>
116
- </div>
117
- )}
118
- {item.curation.caveats.length > 0 && (
119
- <div>
120
- <h3>Caveats</h3>
121
- <ul class="oc-signal-list">{item.curation.caveats.map((value) => <li>{value}</li>)}</ul>
122
- </div>
123
- )}
124
- </div>
125
- </section>
126
- )}
127
- {(languages.length > 0 || fileSignals.length > 0 || monthly.length > 0) && (
128
- <section class="oc-detail-wide">
129
- <h2>Repository Signals</h2>
130
- <div class="oc-repo-signals">
131
- {languages.length > 0 && (
132
- <div>
133
- <h3>Languages</h3>
134
- <dl class="oc-language-bars">
135
- {languages.map(([language, bytes]) => (
136
- <div>
137
- <dt>{language}</dt>
138
- <dd><span style={`width: ${(bytes / totalLanguageBytes) * 100}%`}></span></dd>
139
- </div>
140
- ))}
141
- </dl>
142
- </div>
143
- )}
144
- {fileSignals.length > 0 && (
145
- <div>
146
- <h3>Contribution files</h3>
147
- <ul class="oc-signal-list">
148
- {fileSignals.map(([label, ok]) => <li>{ok ? "Found" : "Missing"}: {label}</li>)}
149
- </ul>
150
- </div>
151
- )}
152
- {monthly.length > 0 && (
153
- <div>
154
- <h3>Monthly activity</h3>
155
- <div class="oc-activity-bars">
156
- {monthly.map((entry) => (
157
- <span title={`${entry.month}: ${entry.commits} commits`} style={`height: ${Math.max(8, (entry.commits / maxCommits) * 48)}px`}></span>
158
- ))}
159
- </div>
160
- </div>
161
- )}
162
- </div>
163
- </section>
164
- )}
165
- </div>
166
- </article>
@@ -1,137 +0,0 @@
1
- ---
2
- interface Props {
3
- repoUrl?: string;
4
- }
5
-
6
- const { repoUrl = "" } = Astro.props;
7
- ---
8
-
9
- <section class="oc-submit-grid">
10
- <form class="oc-submit-form" id="submit-form">
11
- <label>
12
- <span>GitHub URL</span>
13
- <input id="repo-url" type="url" required placeholder="https://github.com/owner/repo" value={repoUrl} />
14
- </label>
15
- <div class="oc-two-col">
16
- <label><span>Name</span><input id="project-name" type="text" /></label>
17
- <label><span>Slug</span><input id="project-slug" type="text" /></label>
18
- </div>
19
- <label><span>Description</span><textarea id="project-description" rows="3"></textarea></label>
20
- <div class="oc-two-col">
21
- <label><span>Category</span><input id="project-category" type="text" placeholder="Developer Tools" /></label>
22
- <label><span>Language</span><input id="project-language" type="text" placeholder="TypeScript" /></label>
23
- </div>
24
- <label><span>Tags</span><input id="project-tags" type="text" placeholder="cli, static-site, search" /></label>
25
- <label><span>Best for</span><textarea id="project-best-for" rows="3" placeholder="One line per use case"></textarea></label>
26
- </form>
27
- <aside class="oc-submit-preview">
28
- <div class="oc-section-header">
29
- <h2>YAML draft</h2>
30
- <button type="button" id="copy-yaml">Copy</button>
31
- </div>
32
- <pre><code id="yaml-preview">Paste a GitHub URL to generate a draft.</code></pre>
33
- <p id="submit-status" aria-live="polite"></p>
34
- </aside>
35
- </section>
36
-
37
- <script>
38
- type FormField = HTMLInputElement | HTMLTextAreaElement | HTMLElement | null;
39
-
40
- const fields: Record<string, FormField> = {
41
- repoUrl: document.querySelector("#repo-url"),
42
- name: document.querySelector("#project-name"),
43
- slug: document.querySelector("#project-slug"),
44
- description: document.querySelector("#project-description"),
45
- category: document.querySelector("#project-category"),
46
- language: document.querySelector("#project-language"),
47
- tags: document.querySelector("#project-tags"),
48
- bestFor: document.querySelector("#project-best-for"),
49
- preview: document.querySelector("#yaml-preview"),
50
- status: document.querySelector("#submit-status"),
51
- copy: document.querySelector("#copy-yaml"),
52
- };
53
-
54
- function valueOf(name: string): string {
55
- const el = fields[name];
56
- if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) return el.value;
57
- return "";
58
- }
59
-
60
- function setText(name: string, text: string): void {
61
- const el = fields[name];
62
- if (el) el.textContent = text;
63
- }
64
-
65
- function slugify(value: string): string {
66
- return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
67
- }
68
-
69
- function parseRepo(value: string): { owner: string; repo: string } | null {
70
- const match = String(value).match(/github\.com\/([^/]+)\/([^/?#]+)/);
71
- if (!match) return null;
72
- return { owner: match[1], repo: match[2].replace(/\.git$/, "") };
73
- }
74
-
75
- function list(values: string[], indent: number): string {
76
- return values.length
77
- ? values.map((v) => `${" ".repeat(indent)}- ${v}`).join("\n")
78
- : `${" ".repeat(indent)}[]`;
79
- }
80
-
81
- function quote(value: string): string {
82
- return `"${String(value ?? "").replace(/"/g, '\\"')}"`;
83
- }
84
-
85
- function generate(): void {
86
- const repo = parseRepo(valueOf("repoUrl"));
87
- if (!repo) {
88
- setText("preview", "Paste a valid GitHub repository URL.");
89
- return;
90
- }
91
- const name = valueOf("name") || repo.repo;
92
- const slug = valueOf("slug") || slugify(name);
93
- const tags = valueOf("tags")
94
- .split(",")
95
- .map((t) => t.trim())
96
- .filter(Boolean);
97
- const bestFor = valueOf("bestFor")
98
- .split("\n")
99
- .map((l) => l.trim())
100
- .filter(Boolean);
101
- const yaml = [
102
- `id: ${slug}`,
103
- `name: ${quote(name)}`,
104
- `description: ${quote(valueOf("description") || name)}`,
105
- "links:",
106
- ` github: https://github.com/${repo.owner}/${repo.repo}`,
107
- "source:",
108
- " type: manual",
109
- "taxonomy:",
110
- ` category: ${quote(valueOf("category") || "uncategorized")}`,
111
- " tags:",
112
- list(tags, 4),
113
- ...(valueOf("language") ? [` language: ${quote(valueOf("language"))}`] : []),
114
- "curation:",
115
- " bestFor:",
116
- list(bestFor, 4),
117
- " whyListed: []",
118
- " caveats: []",
119
- ].join("\n");
120
- setText("preview", yaml);
121
- }
122
-
123
- for (const input of Object.values(fields)) {
124
- if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {
125
- input.addEventListener("input", generate);
126
- }
127
- }
128
- const copyBtn = fields.copy;
129
- if (copyBtn) {
130
- copyBtn.addEventListener("click", async () => {
131
- const previewEl = fields.preview;
132
- await navigator.clipboard.writeText(previewEl?.textContent ?? "");
133
- setText("status", "Copied YAML draft.");
134
- });
135
- }
136
- generate();
137
- </script>