@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.
- package/LICENSE +21 -0
- package/dist/index.d.ts +4 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +56 -22
- package/dist/index.js.map +1 -1
- package/dist/layouts/index.d.ts +25 -0
- package/dist/layouts/index.d.ts.map +1 -0
- package/dist/layouts/index.js +25 -0
- package/dist/layouts/index.js.map +1 -0
- package/dist/lib/display.d.ts +33 -0
- package/dist/lib/display.d.ts.map +1 -0
- package/dist/lib/display.js +104 -0
- package/dist/lib/display.js.map +1 -0
- package/dist/lib/format.d.ts +30 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +73 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/index.d.ts +26 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +26 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/lenses.d.ts +96 -0
- package/dist/lib/lenses.d.ts.map +1 -0
- package/dist/lib/lenses.js +181 -0
- package/dist/lib/lenses.js.map +1 -0
- package/dist/lib/repo.d.ts +30 -0
- package/dist/lib/repo.d.ts.map +1 -0
- package/dist/lib/repo.js +44 -0
- package/dist/lib/repo.js.map +1 -0
- package/dist/lib/scores.d.ts +38 -0
- package/dist/lib/scores.d.ts.map +1 -0
- package/dist/lib/scores.js +83 -0
- package/dist/lib/scores.js.map +1 -0
- package/dist/lib/search.d.ts +199 -0
- package/dist/lib/search.d.ts.map +1 -0
- package/dist/lib/search.js +357 -0
- package/dist/lib/search.js.map +1 -0
- package/dist/lib/taxonomy-counts.d.ts +25 -0
- package/dist/lib/taxonomy-counts.d.ts.map +1 -0
- package/dist/lib/taxonomy-counts.js +41 -0
- package/dist/lib/taxonomy-counts.js.map +1 -0
- package/package.json +7 -7
- package/src/components/AppsIndexRow.astro +204 -0
- package/src/components/AppsPagination.astro +126 -0
- package/src/components/CategoryGrid.astro +86 -17
- package/src/components/ContributorsGrid.astro +149 -0
- package/src/components/DecisionRow.astro +89 -0
- package/src/components/ExploreByCategory.astro +67 -0
- package/src/components/ExploreByStack.astro +72 -0
- package/src/components/Hero.astro +246 -0
- package/src/components/Icon.astro +223 -0
- package/src/components/ItemCard.astro +306 -30
- package/src/components/MinimalAbout.astro +82 -0
- package/src/components/OriginalCollection.astro +116 -0
- package/src/components/RecordSection.astro +88 -0
- package/src/components/RefinePanel.astro +197 -0
- package/src/components/ScoreBars.astro +92 -22
- package/src/components/SmartLensTabs.astro +71 -0
- package/src/components/StackGrid.astro +132 -0
- package/src/components/WhyThisExists.astro +116 -0
- package/src/index.ts +61 -14
- package/src/layouts/BaseLayout.astro +227 -15
- package/src/layouts/Container.astro +57 -0
- package/src/layouts/Footer.astro +235 -0
- package/src/layouts/Header.astro +159 -0
- package/src/layouts/SectionHeader.astro +58 -0
- package/{templates/default/src/components/layout → src/layouts}/Seo.astro +62 -38
- package/src/layouts/ThemeToggle.astro +110 -0
- package/src/layouts/astro-shim.d.ts +29 -0
- package/src/layouts/index.ts +24 -0
- package/src/lib/display.ts +118 -0
- package/src/lib/format.ts +65 -0
- package/src/lib/index.ts +26 -0
- package/src/lib/lenses.ts +241 -0
- package/src/lib/repo.ts +44 -0
- package/src/lib/scores.ts +94 -0
- package/src/lib/search.ts +481 -0
- package/src/lib/taxonomy-counts.ts +60 -0
- package/src/styles.css +366 -638
- package/templates/default/astro.config.mjs +8 -6
- package/templates/default/content/pages/about.md +66 -0
- package/templates/default/content/pages/methodology.md +103 -0
- package/templates/default/data/generated/records.full.json +8 -0
- package/templates/default/data/generated/records.index.json +7 -0
- package/templates/default/data/generated/records.json +8 -0
- package/templates/default/data/generated/site-config.json +31 -0
- package/templates/default/data/records/.gitkeep +0 -0
- package/templates/default/grove.config.ts +36 -0
- package/templates/default/package.json +6 -5
- package/templates/default/public/llms-full.txt +14 -0
- package/templates/default/public/llms.txt +11 -0
- package/templates/default/src/components/layout/BaseLayout.astro +10 -57
- package/templates/default/src/components/layout/Footer.astro +16 -134
- package/templates/default/src/components/layout/Header.astro +25 -126
- package/templates/default/src/data/records.ts +193 -0
- package/templates/default/src/lib/markdown.ts +171 -0
- package/templates/default/src/pages/about.astro +95 -429
- package/templates/default/src/pages/index.astro +263 -61
- package/templates/default/src/pages/projects/[slug].astro +480 -0
- package/templates/default/src/pages/projects/index.astro +193 -0
- package/templates/default/src/pages/sitemap.xml.ts +73 -61
- package/templates/default/src/pages/submit.astro +122 -411
- package/templates/default/src/styles/global.css +42 -68
- package/src/components/ActiveFilterChips.astro +0 -20
- package/src/components/DirectoryFilters.astro +0 -100
- package/src/components/DirectoryHero.astro +0 -32
- package/src/components/HealthBadge.astro +0 -12
- package/src/components/LensTabs.astro +0 -26
- package/src/components/MethodologyPanel.astro +0 -20
- package/src/components/Pagination.astro +0 -24
- package/src/components/ProjectDetail.astro +0 -166
- package/src/components/SubmitDraft.astro +0 -137
- package/templates/default/src/components/AppCard.astro +0 -248
- package/templates/default/src/components/AppSection.astro +0 -58
- package/templates/default/src/components/AppsIndexRow.astro +0 -230
- package/templates/default/src/components/AppsPagination.astro +0 -147
- package/templates/default/src/components/CategoryGrid.astro +0 -80
- package/templates/default/src/components/ContributorsGrid.astro +0 -105
- package/templates/default/src/components/DecisionRow.astro +0 -219
- package/templates/default/src/components/ExploreByCategory.astro +0 -74
- package/templates/default/src/components/ExploreByStack.astro +0 -102
- package/templates/default/src/components/Hero.astro +0 -176
- package/templates/default/src/components/MinimalAbout.astro +0 -57
- package/templates/default/src/components/OriginalCollection.astro +0 -144
- package/templates/default/src/components/RefinePanel.astro +0 -272
- package/templates/default/src/components/ScoreBars.astro +0 -72
- package/templates/default/src/components/SmartLensTabs.astro +0 -65
- package/templates/default/src/components/StackGrid.astro +0 -95
- package/templates/default/src/components/WhyThisExists.astro +0 -81
- package/templates/default/src/components/icons/Icon.astro +0 -148
- package/templates/default/src/components/layout/SectionHeader.astro +0 -42
- package/templates/default/src/components/layout/ThemeToggle.astro +0 -97
- package/templates/default/src/data/apps.ts +0 -149
- package/templates/default/src/data/categories.ts +0 -27
- package/templates/default/src/data/config.ts +0 -28
- package/templates/default/src/data/contributors.ts +0 -96
- package/templates/default/src/data/stacks.ts +0 -47
- package/templates/default/src/data/stats.ts +0 -65
- package/templates/default/src/data/types.ts +0 -248
- package/templates/default/src/lib/apps-search.ts +0 -228
- package/templates/default/src/lib/format.ts +0 -36
- package/templates/default/src/lib/lenses.ts +0 -98
- package/templates/default/src/lib/repo.ts +0 -12
- package/templates/default/src/lib/scores.ts +0 -51
- package/templates/default/src/lib/taxonomy-counts.ts +0 -31
- package/templates/default/src/pages/apps/[slug].astro +0 -709
- package/templates/default/src/pages/apps/index.astro +0 -669
- 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>
|