@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,72 +0,0 @@
1
- ---
2
- import { SCORE_DIMENSIONS, SCORE_LABELS, scoreTier, scoreLabel } from "../lib/scores";
3
- import type { AppScores } from "../data/types";
4
-
5
- interface Props {
6
- scores?: AppScores;
7
- /** Show the "Overall" dimension too (default true). */
8
- showOverall?: boolean;
9
- /** Compact mode: hide value text, bar only. */
10
- compact?: boolean;
11
- class?: string;
12
- }
13
-
14
- const { scores, showOverall = true, compact = false, class: className = "" } = Astro.props;
15
- const dims = showOverall ? SCORE_DIMENSIONS : SCORE_DIMENSIONS.filter((d) => d !== "overall");
16
- const hasAny = scores && Object.values(scores).some((v) => typeof v === "number");
17
- ---
18
-
19
- <dl
20
- class={`flex flex-wrap items-center gap-x-3 gap-y-1.5 text-2xs ${className}`}
21
- aria-label="App scores"
22
- >
23
- {!hasAny && (
24
- <span class="text-ink-400 dark:text-ink-500">Scores not yet curated</span>
25
- )}
26
- {hasAny &&
27
- dims.map((dim) => {
28
- const v = scores![dim];
29
- const tier = typeof v === "number" ? scoreTier(v) : 0;
30
- const isOverall = dim === "overall";
31
- return (
32
- <div class="inline-flex items-center gap-1.5">
33
- <dt
34
- class={`${
35
- isOverall
36
- ? "font-semibold text-ink-700 dark:text-ink-200"
37
- : "text-ink-500 dark:text-ink-400"
38
- }`}
39
- >
40
- {SCORE_LABELS[dim]}
41
- </dt>
42
- {!compact && (
43
- <dd
44
- class={`font-mono ${
45
- isOverall
46
- ? "text-ink-900 dark:text-ink-100"
47
- : "text-ink-700 dark:text-ink-300"
48
- }`}
49
- >
50
- {scoreLabel(v)}
51
- </dd>
52
- )}
53
- <dd
54
- class="flex h-1.5 w-10 gap-px"
55
- aria-label={`${SCORE_LABELS[dim]} ${scoreLabel(v)} of 100`}
56
- >
57
- {[0, 1, 2, 3].map((i) => (
58
- <div
59
- class={`flex-1 rounded-sm ${
60
- i < tier
61
- ? isOverall
62
- ? "bg-ink-700 dark:bg-ink-200"
63
- : "bg-ink-500 dark:bg-ink-400"
64
- : "bg-ink-200 dark:bg-ink-800"
65
- }`}
66
- />
67
- ))}
68
- </dd>
69
- </div>
70
- );
71
- })}
72
- </dl>
@@ -1,65 +0,0 @@
1
- ---
2
- import { LENSES, PRIMARY_LENSES, isLensActive, type LensId } from "../lib/lenses";
3
-
4
- interface Props {
5
- currentParams: URLSearchParams;
6
- class?: string;
7
- }
8
-
9
- const { currentParams, class: className = "" } = Astro.props;
10
-
11
- // Render only the 6 primary lenses, in declared order.
12
- const tabs = PRIMARY_LENSES.map((id) => LENSES.find((l) => l.id === id)!).filter(Boolean);
13
-
14
- // Build href for a lens: merge lens's params on top of current search.
15
- function hrefFor(lensId: LensId): string {
16
- const sp = new URLSearchParams(currentParams);
17
- // Clear any existing lens
18
- sp.delete("lens");
19
- if (lensId !== "all") {
20
- const def = LENSES.find((l) => l.id === lensId);
21
- if (def) {
22
- for (const [k, v] of Object.entries(def.toParams())) {
23
- if (Array.isArray(v)) {
24
- sp.delete(k);
25
- for (const item of v) sp.append(k, item);
26
- } else {
27
- sp.set(k, v);
28
- }
29
- }
30
- }
31
- }
32
- const qs = sp.toString();
33
- return qs ? `/apps?${qs}` : "/apps";
34
- }
35
- ---
36
-
37
- <nav
38
- class={`overflow-x-auto ${className}`}
39
- aria-label="Curated lenses"
40
- >
41
- <ul class="m-0 flex list-none items-center gap-1 p-0 whitespace-nowrap">
42
- {
43
- tabs.map((lens) => {
44
- const isActive = isLensActive(lens.id, currentParams);
45
- return (
46
- <li>
47
- <a
48
- href={hrefFor(lens.id)}
49
- data-lens-id={lens.id}
50
- class={`lens-tab inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-2xs font-medium transition-colors ${
51
- isActive
52
- ? "border-ink-900 bg-ink-900 text-white dark:border-ink-100 dark:bg-ink-100 dark:text-ink-900"
53
- : "border-ink-200 bg-white text-ink-700 hover:border-ink-300 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:hover:bg-ink-900/60"
54
- }`}
55
- aria-current={isActive ? "page" : undefined}
56
- title={lens.description}
57
- >
58
- {lens.label}
59
- </a>
60
- </li>
61
- );
62
- })
63
- }
64
- </ul>
65
- </nav>
@@ -1,95 +0,0 @@
1
- ---
2
- import type { Stack } from "../data/stacks";
3
- import SectionHeader from "./layout/SectionHeader.astro";
4
- import Container from "./layout/Container.astro";
5
- import Icon from "./icons/Icon.astro";
6
- import { countByStack } from "../lib/taxonomy-counts";
7
-
8
- interface Props {
9
- stacks: Stack[];
10
- }
11
-
12
- const { stacks } = Astro.props;
13
- const counts = countByStack();
14
-
15
- const statusLabel: Record<Stack["status"], string> = {
16
- live: "Available now",
17
- expanding: "Expanding",
18
- planned: "Planned",
19
- };
20
-
21
- const statusBadge: Record<Stack["status"], string> = {
22
- live: "border-emerald-300/60 bg-emerald-50 text-emerald-800 dark:border-emerald-800/60 dark:bg-emerald-950/40 dark:text-emerald-300",
23
- expanding: "border-ink-200 bg-ink-50 text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300",
24
- planned: "border-ink-200 bg-white text-ink-500 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-500",
25
- };
26
-
27
- // Decorative count for each stack. We render it as a small "n apps" pill
28
- // for live stacks and as a "0 apps" / "Planned" pill for the rest so the
29
- // UI is honest about coverage — no fake numbers, no "Available now"
30
- // stacks that secretly have zero apps.
31
- const enriched = stacks
32
- .map((s) => ({ ...s, count: counts.get(s.slug) ?? 0 }))
33
- .sort((a, b) => b.count - a.count);
34
- ---
35
-
36
- <section
37
- id="stacks"
38
- class="section scroll-mt-20 border-b border-ink-200 dark:border-ink-800"
39
- >
40
- <Container>
41
- <SectionHeader
42
- eyebrow="Stacks"
43
- title="Browse by stack"
44
- description="Pick a stack to see real apps built with it."
45
- />
46
-
47
- <div class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
48
- {
49
- enriched.map((stack) => (
50
- <a
51
- href={`/apps?stack=${encodeURIComponent(stack.name)}`}
52
- class="card card-hover group flex h-full items-center gap-3 p-3.5 no-underline"
53
- >
54
- {/* Compact stack icon — supports the card without dominating it */}
55
- <div
56
- class="flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-ink-200 bg-white dark:border-ink-800 dark:bg-ink-950"
57
- aria-hidden="true"
58
- >
59
- <Icon
60
- name={stack.name}
61
- category="stack"
62
- size={18}
63
- class="h-[18px] w-[18px]"
64
- />
65
- </div>
66
-
67
- <div class="flex min-w-0 flex-1 flex-col gap-0.5">
68
- <div class="flex items-center gap-2">
69
- <h3 class="m-0 text-[0.875rem] font-semibold tracking-tight text-ink-900 group-hover:underline dark:text-ink-100">
70
- {stack.name}
71
- </h3>
72
- <span
73
- class="inline-flex items-center rounded-md border border-ink-200 bg-ink-50 px-1.5 py-0.5 font-mono text-[10px] font-medium text-ink-600 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-400"
74
- title={`${stack.count} apps in this stack`}
75
- >
76
- {stack.count} apps
77
- </span>
78
- </div>
79
- <p class="m-0 truncate text-2xs text-ink-500 dark:text-ink-400">
80
- {stack.blurb}
81
- </p>
82
- </div>
83
-
84
- <span
85
- class={`shrink-0 inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wider ${statusBadge[stack.status]}`}
86
- aria-label={`Status: ${statusLabel[stack.status]}`}
87
- >
88
- {statusLabel[stack.status]}
89
- </span>
90
- </a>
91
- ))
92
- }
93
- </div>
94
- </Container>
95
- </section>
@@ -1,81 +0,0 @@
1
- ---
2
- import Container from "./layout/Container.astro";
3
-
4
- const points = [
5
- {
6
- title: "Real apps",
7
- body: "Not SDKs, templates, or tiny demos. Focused on complete applications you can actually run.",
8
- icon: "app",
9
- },
10
- {
11
- title: "Codebase context",
12
- body: "Stack, platform, license, activity, maturity, and contribution signals — one card per app.",
13
- icon: "code",
14
- },
15
- {
16
- title: "Searchable discovery",
17
- body: "Browse by category, stack, platform, stars, recency, and status — not a flat README list.",
18
- icon: "search",
19
- },
20
- ];
21
- ---
22
-
23
- <section id="about" class="section scroll-mt-20 border-b border-ink-200 dark:border-ink-800">
24
- <Container>
25
- <div class="flex flex-col gap-2">
26
- <span
27
- class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400"
28
- >Why this exists</span
29
- >
30
- <h2
31
- class="text-[1.5rem] sm:text-[1.75rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100 m-0 max-w-[40ch]"
32
- >
33
- Most open-source app discovery is fragmented.
34
- </h2>
35
- <p
36
- class="text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400 m-0 max-w-[64ch]"
37
- >
38
- GitHub search shows repositories, not always usable products. Awesome
39
- lists are helpful, but they tend to become long static README files.
40
- This project focuses on real applications: projects with source code,
41
- product context, stack information, activity signals, and enough
42
- structure to help developers learn, compare, and contribute.
43
- </p>
44
- </div>
45
-
46
- <ul
47
- class="mt-7 grid list-none grid-cols-1 gap-3 p-0 m-0 sm:grid-cols-3"
48
- >
49
- {
50
- points.map((p) => (
51
- <li class="card p-4">
52
- <div
53
- class="mb-3 flex h-8 w-8 items-center justify-center rounded-md border border-ink-200 bg-white text-ink-700 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300"
54
- aria-hidden="true"
55
- >
56
- {p.icon === "app" ? (
57
- <svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
58
- <path d="M3 2.75A2.25 2.25 0 0 1 5.25.5h5.5A2.25 2.25 0 0 1 13 2.75v10.5A2.25 2.25 0 0 1 10.75 15.5h-5.5A2.25 2.25 0 0 1 3 13.25V2.75ZM5.25 2a.75.75 0 0 0-.75.75v10.5c0 .414.336.75.75.75h5.5a.75.75 0 0 0 .75-.75V2.75a.75.75 0 0 0-.75-.75h-5.5Z"/>
59
- </svg>
60
- ) : p.icon === "code" ? (
61
- <svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
62
- <path d="M4.72 3.22a.75.75 0 0 1 1.06 1.06L2.06 8l3.72 3.72a.75.75 0 1 1-1.06 1.06L.47 8.53a.75.75 0 0 1 0-1.06l4.25-4.25Zm6.56 0a.75.75 0 1 1 1.06-1.06l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L13.94 8l-3.66-3.72Z"/>
63
- </svg>
64
- ) : (
65
- <svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
66
- <path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"/>
67
- </svg>
68
- )}
69
- </div>
70
- <h3 class="m-0 text-[0.9375rem] font-semibold text-ink-900 dark:text-ink-100">
71
- {p.title}
72
- </h3>
73
- <p class="mt-1.5 m-0 text-[0.8125rem] leading-relaxed text-ink-500 dark:text-ink-400">
74
- {p.body}
75
- </p>
76
- </li>
77
- ))
78
- }
79
- </ul>
80
- </Container>
81
- </section>
@@ -1,148 +0,0 @@
1
- ---
2
- // Brand icon registry.
3
- //
4
- // Strategy: icons live as static SVGs in `public/icons/`. The fetch
5
- // script (`scripts/fetch-icons.mjs`) downloads the well-known
6
- // brand icons from the public `xandemon/developer-icons` repo on
7
- // prebuild. Missing icons (windows, web, desktop, chromeos,
8
- // embedded, ionic, capacitor) are hand-rolled in `public/icons/`.
9
- //
10
- // Theme-aware variants: some icons (Apple for iOS / macOS) ship
11
- // in light and dark silhouettes and need to be swapped with
12
- // the page's color scheme. The Icon component renders BOTH img
13
- // tags and uses Tailwind's `dark:hidden` / `hidden dark:block` to
14
- // show only the right one.
15
- //
16
- // Usage:
17
- // <Icon name="Flutter" category="stack" size={12} />
18
- // <Icon name="iOS" category="platform" size={12} />
19
- //
20
- // Falls back to a small generic dot if no icon is found so the chip
21
- // still has visual weight.
22
-
23
- interface Props {
24
- /** Friendly name, e.g. "Flutter", "React Native", "iOS", "Web". */
25
- name: string;
26
- /** "stack" or "platform" — controls where we look in /icons/. */
27
- category?: "stack" | "platform";
28
- size?: number;
29
- class?: string;
30
- title?: string;
31
- }
32
-
33
- const { name, category = "stack", size = 12, class: className = "", title } = Astro.props;
34
-
35
- function slugify(s: string): string {
36
- return s
37
- .toLowerCase()
38
- .trim()
39
- .replace(/\+/g, "plus")
40
- .replace(/\./g, "")
41
- .replace(/[\s/]+/g, "-")
42
- .replace(/-+/g, "-")
43
- .replace(/^-|-$/g, "")
44
- .replace(/[^a-z0-9-]/g, "");
45
- }
46
-
47
- // Friendly name → icon filename. Add a new alias when a chip needs
48
- // to render an icon that lives under a different filename.
49
- const ALIASES: Record<string, string> = {
50
- // stacks
51
- react: "react",
52
- "react-native": "react-native",
53
- "node-js": "nodejs",
54
- nodejs: "nodejs",
55
- "node.js": "nodejs",
56
- mongodb: "mongodb",
57
- tensorflow: "tensorflow",
58
- ionic: "ionic",
59
- "ionic-capacitor": "ionic",
60
- ionicframework: "ionic",
61
- capacitor: "capacitor", // distinct from ionic — proper Capacitor brand mark
62
- "kotlin-multiplatform": "kotlin",
63
- kmp: "kotlin",
64
- // platforms
65
- ios: "ios",
66
- ipados: "ios",
67
- macos: "macos",
68
- osx: "macos",
69
- "mac-os": "macos",
70
- web: "web",
71
- chromeos: "chromeos",
72
- "chrome-os": "chromeos",
73
- windows: "windows",
74
- win: "windows",
75
- desktop: "desktop",
76
- embedded: "embedded",
77
- };
78
-
79
- // These icons have explicit light + dark variants. We render both
80
- // and let CSS swap based on the `.dark` class on <html>.
81
- const THEME_AWARE = new Set([
82
- "ios", // maps to apple-dark (light) + apple-light (dark)
83
- "macos", // maps to apple-light (light) + apple-dark (dark)
84
- ]);
85
-
86
- const slug = slugify(name);
87
- const resolved = ALIASES[slug] ?? slug;
88
- const baseDir = category === "platform" ? "platforms" : "stacks";
89
- const url = (file: string) => `/icons/${baseDir}/${file}.svg`;
90
-
91
- const isThemeAware = THEME_AWARE.has(resolved);
92
-
93
- // Map theme-aware platform → { light: file, dark: file }
94
- const THEME_PAIRS: Record<string, { light: string; dark: string }> = {
95
- ios: { light: "apple-dark", dark: "apple-light" },
96
- macos: { light: "apple-light", dark: "apple-dark" },
97
- };
98
- ---
99
-
100
- {
101
- isThemeAware ? (
102
- <>
103
- {/* Light theme → light variant */}
104
- <img
105
- src={url(THEME_PAIRS[resolved].light)}
106
- alt=""
107
- width={size}
108
- height={size}
109
- loading="lazy"
110
- decoding="async"
111
- aria-hidden={title ? undefined : "true"}
112
- class={`inline-block shrink-0 object-contain select-none dark:hidden ${className}`}
113
- style={`width:${size}px;height:${size}px;`}
114
- />
115
- {/* Dark theme → dark variant */}
116
- <img
117
- src={url(THEME_PAIRS[resolved].dark)}
118
- alt=""
119
- width={size}
120
- height={size}
121
- loading="lazy"
122
- decoding="async"
123
- aria-hidden={title ? undefined : "true"}
124
- class={`inline-block shrink-0 object-contain select-none hidden dark:inline-block ${className}`}
125
- style={`width:${size}px;height:${size}px;`}
126
- />
127
- </>
128
- ) : resolved ? (
129
- <img
130
- src={url(resolved)}
131
- alt=""
132
- width={size}
133
- height={size}
134
- loading="lazy"
135
- decoding="async"
136
- aria-hidden={title ? undefined : "true"}
137
- class={`inline-block shrink-0 object-contain select-none ${className}`}
138
- style={`width:${size}px;height:${size}px;`}
139
- />
140
- ) : (
141
- <span
142
- aria-hidden="true"
143
- class={`inline-block shrink-0 rounded-full bg-current opacity-40 ${className}`}
144
- style={`width:${size}px;height:${size}px;`}
145
- />
146
- )
147
- }
148
- {title && <span class="sr-only">{title}</span>}
@@ -1,42 +0,0 @@
1
- ---
2
- interface Props {
3
- eyebrow?: string;
4
- title: string;
5
- description?: string;
6
- align?: "left" | "center";
7
- level?: 2 | 3;
8
- }
9
-
10
- const { eyebrow, title, description, align = "left", level = 2 } = Astro.props;
11
- const Heading = `h${level}` as "h2" | "h3";
12
- ---
13
-
14
- <header
15
- class={`mb-6 flex flex-col gap-2 ${
16
- align === "center" ? "items-center text-center" : ""
17
- }`}
18
- >
19
- {
20
- eyebrow && (
21
- <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
22
- {eyebrow}
23
- </span>
24
- )
25
- }
26
- <Heading
27
- class="text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100 m-0"
28
- >
29
- {title}
30
- </Heading>
31
- {
32
- description && (
33
- <p
34
- class={`text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400 m-0 ${
35
- align === "center" ? "max-w-2xl" : "max-w-[56ch]"
36
- }`}
37
- >
38
- {description}
39
- </p>
40
- )
41
- }
42
- </header>
@@ -1,97 +0,0 @@
1
- ---
2
- // A three-position theme switcher: light / dark / system.
3
- //
4
- // State is persisted in `localStorage` under the key "open-apps-theme".
5
- // On mount it reads the stored value (or "system" if none), applies
6
- // the corresponding class to <html>, and re-syncs when the OS
7
- // preference changes (only meaningful when mode is "system").
8
- //
9
- // Implementation note: the actual <html class="dark"> toggle happens
10
- // in the inline script in `src/components/layout/BaseLayout.astro`
11
- // (THEME_INIT). That script runs before paint so the user never sees
12
- // a flash of the wrong theme.
13
-
14
- interface Props {
15
- class?: string;
16
- }
17
-
18
- const { class: className = "" } = Astro.props;
19
- ---
20
-
21
- <button
22
- type="button"
23
- id="theme-toggle"
24
- data-theme-toggle
25
- aria-label="Toggle theme"
26
- title="Toggle theme"
27
- class={`relative inline-flex h-8 w-8 items-center justify-center rounded-md border border-ink-200 bg-white text-ink-700 transition-colors hover:border-ink-300 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-200 dark:hover:border-ink-700 dark:hover:bg-ink-900/40 ${className}`}
28
- >
29
- {/* Sun — shown in dark mode (click to go light) */}
30
- <svg
31
- class="h-4 w-4 hidden dark:block"
32
- viewBox="0 0 24 24"
33
- fill="none"
34
- stroke="currentColor"
35
- stroke-width="1.8"
36
- stroke-linecap="round"
37
- stroke-linejoin="round"
38
- aria-hidden="true"
39
- >
40
- <circle cx="12" cy="12" r="4"/>
41
- <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
42
- </svg>
43
- {/* Moon — shown in light mode (click to go dark) */}
44
- <svg
45
- class="h-4 w-4 block dark:hidden"
46
- viewBox="0 0 24 24"
47
- fill="none"
48
- stroke="currentColor"
49
- stroke-width="1.8"
50
- stroke-linecap="round"
51
- stroke-linejoin="round"
52
- aria-hidden="true"
53
- >
54
- <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79Z"/>
55
- </svg>
56
- </button>
57
-
58
- <script>
59
- // The toggle button simply rotates between the three modes on
60
- // click. The actual class application lives in the page-level
61
- // THEME_INIT script so behaviour stays consistent.
62
- const STORAGE_KEY = "open-apps-theme";
63
- type Mode = "light" | "dark" | "system";
64
-
65
- function currentMode(): Mode {
66
- const stored = localStorage.getItem(STORAGE_KEY);
67
- return stored === "light" || stored === "dark" || stored === "system"
68
- ? stored
69
- : "system";
70
- }
71
-
72
- function rotate(): Mode {
73
- const order: Mode[] = ["light", "dark", "system"];
74
- const next = order[(order.indexOf(currentMode()) + 1) % order.length];
75
- localStorage.setItem(STORAGE_KEY, next);
76
- // Notify the page-level listener.
77
- window.dispatchEvent(new CustomEvent("theme:set", { detail: next }));
78
- return next;
79
- }
80
-
81
- // A small tooltip label so the user knows what mode they'll get
82
- // on the next click. We re-read current mode on focus/hover so it
83
- // stays in sync.
84
- function updateLabel(btn: HTMLButtonElement) {
85
- btn.title = `Theme: ${currentMode()} (click to switch)`;
86
- }
87
-
88
- document.querySelectorAll<HTMLButtonElement>("[data-theme-toggle]").forEach((btn) => {
89
- updateLabel(btn);
90
- btn.addEventListener("click", () => {
91
- rotate();
92
- updateLabel(btn);
93
- });
94
- btn.addEventListener("mouseenter", () => updateLabel(btn));
95
- btn.addEventListener("focus", () => updateLabel(btn));
96
- });
97
- </script>