@grove-dev/astro 0.6.1 → 0.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.
- package/README.md +77 -5
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -33
- package/dist/index.js.map +1 -1
- package/dist/lib/display.d.ts +1 -1
- package/dist/lib/display.js +1 -1
- package/dist/lib/format.d.ts +1 -1
- package/dist/lib/format.js +1 -1
- package/dist/lib/icon-kinds.d.ts +1 -1
- package/dist/lib/icon-kinds.js +43 -43
- package/dist/lib/icon-registry.d.ts +2 -2
- package/dist/lib/icon-registry.js +23 -23
- package/dist/lib/index.d.ts +9 -9
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +9 -9
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/lenses.d.ts +1 -1
- package/dist/lib/lenses.js +1 -1
- package/dist/lib/packaged-icons.d.ts +1 -1
- package/dist/lib/packaged-icons.js +4 -4
- package/dist/lib/repo.d.ts +1 -1
- package/dist/lib/repo.js +1 -1
- package/dist/lib/search.d.ts +1 -1
- package/dist/lib/search.js +1 -1
- package/dist/lib/taxonomy-counts.d.ts +1 -1
- package/dist/lib/taxonomy-counts.js +1 -1
- package/package.json +7 -17
- package/src/index.ts +23 -38
- package/src/lib/display.ts +1 -1
- package/src/lib/format.ts +1 -1
- package/src/lib/icon-kinds.ts +44 -44
- package/src/lib/icon-registry.ts +25 -25
- package/src/lib/index.ts +9 -9
- package/src/lib/lenses.ts +1 -1
- package/src/lib/packaged-icons.ts +8 -8
- package/src/lib/repo.ts +1 -1
- package/src/lib/search.ts +1 -1
- package/src/lib/taxonomy-counts.ts +1 -1
- package/src/server/collections.ts +53 -42
- package/src/server/contrast.ts +14 -9
- package/src/server/directory.ts +22 -17
- package/src/server/index.ts +3 -2
- package/src/server/models.ts +404 -213
- package/src/server/seo.ts +34 -31
- package/src/styles.css +235 -91
- package/dist/layouts/index.d.ts +0 -25
- package/dist/layouts/index.d.ts.map +0 -1
- package/dist/layouts/index.js +0 -25
- package/dist/layouts/index.js.map +0 -1
- package/dist/ui/button.d.ts +0 -53
- package/dist/ui/button.d.ts.map +0 -1
- package/dist/ui/button.js +0 -90
- package/dist/ui/button.js.map +0 -1
- package/src/components/CardGrid.astro +0 -41
- package/src/components/CardIcon.astro +0 -67
- package/src/components/CategoryGrid.astro +0 -114
- package/src/components/CollectionCard.astro +0 -56
- package/src/components/CollectionIndex.astro +0 -102
- package/src/components/CollectionPage.astro +0 -128
- package/src/components/CollectionRow.astro +0 -81
- package/src/components/CollectionTeaser.astro +0 -47
- package/src/components/ContributorsGrid.astro +0 -143
- package/src/components/DirectoryIndexClient.astro +0 -510
- package/src/components/EditorialSummary.astro +0 -58
- package/src/components/FilterGroupMenu.astro +0 -69
- package/src/components/FilterOptions.astro +0 -49
- package/src/components/FinalCta.astro +0 -43
- package/src/components/Hero.astro +0 -243
- package/src/components/Icon.astro +0 -191
- package/src/components/IndexRow.astro +0 -37
- package/src/components/LanguageBreakdown.astro +0 -67
- package/src/components/MarkdownBody.astro +0 -37
- package/src/components/OriginalCollection.astro +0 -117
- package/src/components/Pagination.astro +0 -50
- package/src/components/PoweredBy.astro +0 -60
- package/src/components/ProjectCard.astro +0 -321
- package/src/components/RecordHeader.astro +0 -221
- package/src/components/RecordSection.astro +0 -128
- package/src/components/RecordSidebar.astro +0 -387
- package/src/components/RefinePanel.astro +0 -168
- package/src/components/SmartLensTabs.astro +0 -81
- package/src/components/StackGrid.astro +0 -151
- package/src/components/StackPlatformChips.astro +0 -73
- package/src/components/SubmissionClient.astro +0 -254
- package/src/components/TableOfContents.astro +0 -140
- package/src/components/WhyThisExists.astro +0 -116
- package/src/layouts/BaseLayout.astro +0 -415
- package/src/layouts/Container.astro +0 -18
- package/src/layouts/Footer.astro +0 -231
- package/src/layouts/Header.astro +0 -214
- package/src/layouts/SectionHeader.astro +0 -43
- package/src/layouts/Seo.astro +0 -171
- package/src/layouts/ThemeToggle.astro +0 -118
- package/src/layouts/astro-shim.d.ts +0 -29
- package/src/layouts/index.ts +0 -24
- package/src/server/collections.test.ts +0 -212
- package/src/server/contrast.test.ts +0 -82
- package/src/server/directory.test.ts +0 -177
- package/src/server/github-repo.test.ts +0 -88
- package/src/server/models-home.test.ts +0 -101
- package/src/server/models.test.ts +0 -135
- package/src/server/seo.test.ts +0 -123
- package/src/ui/Badge.astro +0 -38
- package/src/ui/Button.astro +0 -42
- package/src/ui/EmptyState.astro +0 -38
- package/src/ui/FilterDrawer.astro +0 -124
- package/src/ui/PageHeader.astro +0 -39
- package/src/ui/SearchField.astro +0 -106
- package/src/ui/button.test.ts +0 -54
- package/src/ui/button.ts +0 -107
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* FilterDrawer — mobile filter surface built on native `<dialog>`.
|
|
4
|
-
*
|
|
5
|
-
* `showModal()` gives focus trapping, Escape handling, and the
|
|
6
|
-
* top-layer backdrop for free — no dependency, no hand-rolled trap.
|
|
7
|
-
* On viewports ≥ md the trigger button hides and the slotted filter
|
|
8
|
-
* controls render inline through the same slot used on desktop, so
|
|
9
|
-
* there is exactly one copy of the filter markup.
|
|
10
|
-
*
|
|
11
|
-
* No-JS fallback: the slot content stays in normal flow (the dialog
|
|
12
|
-
* only wraps it once the client script moves it), so filters remain
|
|
13
|
-
* usable without JavaScript.
|
|
14
|
-
*/
|
|
15
|
-
import { buttonClass } from "./button.js";
|
|
16
|
-
|
|
17
|
-
interface Props {
|
|
18
|
-
/** Trigger button label. */
|
|
19
|
-
label?: string;
|
|
20
|
-
/** Count of active filters shown on the trigger. */
|
|
21
|
-
activeCount?: number;
|
|
22
|
-
/** URL that clears every filter — offered inside the sheet. */
|
|
23
|
-
clearHref?: string;
|
|
24
|
-
class?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const {
|
|
28
|
-
label = "Filters",
|
|
29
|
-
activeCount = 0,
|
|
30
|
-
clearHref,
|
|
31
|
-
class: className = "",
|
|
32
|
-
} = Astro.props;
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
<div class={`grove-filter-drawer ${className}`} data-grove-filter-drawer>
|
|
36
|
-
<button
|
|
37
|
-
type="button"
|
|
38
|
-
data-drawer-open
|
|
39
|
-
class={buttonClass("secondary", "md", "w-full md:hidden")}
|
|
40
|
-
aria-haspopup="dialog"
|
|
41
|
-
>
|
|
42
|
-
{label}
|
|
43
|
-
{/* Always in the DOM (hidden at zero) so the client script can
|
|
44
|
-
update it from the live URL state on this prerendered page. */}
|
|
45
|
-
<span
|
|
46
|
-
data-drawer-count
|
|
47
|
-
class:list={[
|
|
48
|
-
"inline-flex min-w-5 items-center justify-center rounded-full bg-selected px-1.5 py-0.5 font-mono text-2xs leading-none text-selected-foreground",
|
|
49
|
-
activeCount === 0 && "hidden",
|
|
50
|
-
]}
|
|
51
|
-
>
|
|
52
|
-
{activeCount}
|
|
53
|
-
</span>
|
|
54
|
-
</button>
|
|
55
|
-
|
|
56
|
-
{/* On mobile the client script moves this content into the dialog;
|
|
57
|
-
on ≥ md it stays inline. Not hidden by CSS — without JS the
|
|
58
|
-
filters must remain visible in flow on every viewport. */}
|
|
59
|
-
<div data-drawer-content class="md:contents">
|
|
60
|
-
<slot />
|
|
61
|
-
</div>
|
|
62
|
-
|
|
63
|
-
<dialog
|
|
64
|
-
data-drawer-dialog
|
|
65
|
-
class="m-0 mt-auto w-full max-w-none rounded-t-[calc(var(--radius)+0.5rem)] border-0 bg-surface-overlay p-0 text-foreground backdrop:bg-black/40 md:hidden"
|
|
66
|
-
aria-label={label}
|
|
67
|
-
>
|
|
68
|
-
<div class="flex items-center justify-between gap-3 border-b border-border px-4 py-3">
|
|
69
|
-
<p class="m-0 text-sm font-semibold">{label}</p>
|
|
70
|
-
<div class="flex items-center gap-2">
|
|
71
|
-
{clearHref && (
|
|
72
|
-
/* Clearing from inside the sheet is the only way out of a
|
|
73
|
-
stack of filters on mobile — the chips row is behind it. */
|
|
74
|
-
<a
|
|
75
|
-
href={clearHref}
|
|
76
|
-
data-drawer-clear
|
|
77
|
-
class:list={[buttonClass("ghost", "sm"), activeCount === 0 && "hidden"]}
|
|
78
|
-
>
|
|
79
|
-
Clear all
|
|
80
|
-
</a>
|
|
81
|
-
)}
|
|
82
|
-
<button type="button" data-drawer-close class={buttonClass("ghost", "sm")}>
|
|
83
|
-
Done
|
|
84
|
-
</button>
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
<div data-drawer-body class="max-h-[70vh] overflow-y-auto p-4"></div>
|
|
88
|
-
</dialog>
|
|
89
|
-
</div>
|
|
90
|
-
|
|
91
|
-
<script>
|
|
92
|
-
document.querySelectorAll<HTMLElement>("[data-grove-filter-drawer]").forEach((root) => {
|
|
93
|
-
const dialog = root.querySelector<HTMLDialogElement>("[data-drawer-dialog]");
|
|
94
|
-
const content = root.querySelector<HTMLElement>("[data-drawer-content]");
|
|
95
|
-
const body = root.querySelector<HTMLElement>("[data-drawer-body]");
|
|
96
|
-
const open = root.querySelector<HTMLButtonElement>("[data-drawer-open]");
|
|
97
|
-
const close = root.querySelector<HTMLButtonElement>("[data-drawer-close]");
|
|
98
|
-
if (!dialog || !content || !body || !open) return;
|
|
99
|
-
|
|
100
|
-
const mobile = window.matchMedia("(max-width: 47.999rem)");
|
|
101
|
-
const place = () => {
|
|
102
|
-
// One copy of the filter markup, moved between the inline slot
|
|
103
|
-
// (desktop) and the dialog body (mobile).
|
|
104
|
-
if (mobile.matches) {
|
|
105
|
-
body.append(...content.children);
|
|
106
|
-
content.classList.add("hidden");
|
|
107
|
-
} else {
|
|
108
|
-
if (dialog.open) dialog.close();
|
|
109
|
-
content.append(...body.children);
|
|
110
|
-
content.classList.remove("hidden");
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
place();
|
|
114
|
-
mobile.addEventListener("change", place);
|
|
115
|
-
|
|
116
|
-
open.addEventListener("click", () => dialog.showModal());
|
|
117
|
-
close?.addEventListener("click", () => dialog.close());
|
|
118
|
-
dialog.addEventListener("click", (event) => {
|
|
119
|
-
// Backdrop click closes: the dialog element itself is the
|
|
120
|
-
// target only when the click lands outside the panel content.
|
|
121
|
-
if (event.target === dialog) dialog.close();
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
</script>
|
package/src/ui/PageHeader.astro
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* PageHeader — the page's title block. Always renders a semantic
|
|
4
|
-
* heading (h1 by default — every route needs exactly one) with an
|
|
5
|
-
* optional eyebrow and description.
|
|
6
|
-
*
|
|
7
|
-
* For section headings INSIDE a page keep using `SectionHeader`
|
|
8
|
-
* (layouts/), which defaults to h2/h3.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
interface Props {
|
|
12
|
-
eyebrow?: string;
|
|
13
|
-
title: string;
|
|
14
|
-
description?: string;
|
|
15
|
-
/** Escape hatch for embedded use; the default 1 is the point. */
|
|
16
|
-
level?: 1 | 2;
|
|
17
|
-
class?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const { eyebrow, title, description, level = 1, class: className = "" } = Astro.props;
|
|
21
|
-
const Heading = `h${level}` as "h1" | "h2";
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
<header class={`flex flex-col gap-1 ${className}`}>
|
|
25
|
-
{eyebrow && (
|
|
26
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
27
|
-
{eyebrow}
|
|
28
|
-
</span>
|
|
29
|
-
)}
|
|
30
|
-
<Heading class="m-0 text-[1.875rem] font-semibold tracking-tight text-foreground sm:text-[2.125rem]">
|
|
31
|
-
{title}
|
|
32
|
-
</Heading>
|
|
33
|
-
{description && (
|
|
34
|
-
<p class="m-0 mt-1 max-w-[64ch] text-[0.9375rem] leading-relaxed text-muted-foreground">
|
|
35
|
-
{description}
|
|
36
|
-
</p>
|
|
37
|
-
)}
|
|
38
|
-
<slot />
|
|
39
|
-
</header>
|
package/src/ui/SearchField.astro
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* SearchField — labeled search input with the leading glass icon.
|
|
4
|
-
*
|
|
5
|
-
* The label is visually hidden but always present; placeholder text
|
|
6
|
-
* is display copy, not the accessible name. Pass the derived
|
|
7
|
-
* placeholder from `getDirectoryIndexModel().searchPlaceholder` so it
|
|
8
|
-
* only names ENABLED facets.
|
|
9
|
-
*
|
|
10
|
-
* `shortcut` renders the `/` hint and wires the global key handler.
|
|
11
|
-
* The hint is hidden once the field has focus — it is an invitation,
|
|
12
|
-
* not a persistent label — and on coarse pointers, where there is no
|
|
13
|
-
* key to press.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
interface Props {
|
|
17
|
-
label: string;
|
|
18
|
-
name?: string;
|
|
19
|
-
id?: string;
|
|
20
|
-
value?: string;
|
|
21
|
-
placeholder?: string;
|
|
22
|
-
/** Show the `/` hint and focus this field when `/` is pressed. */
|
|
23
|
-
shortcut?: boolean;
|
|
24
|
-
class?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const {
|
|
28
|
-
label,
|
|
29
|
-
name = "q",
|
|
30
|
-
id = "search-input",
|
|
31
|
-
value = "",
|
|
32
|
-
placeholder,
|
|
33
|
-
shortcut = false,
|
|
34
|
-
class: className = "",
|
|
35
|
-
} = Astro.props;
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
<label class={`grove-search relative block ${className}`}>
|
|
39
|
-
<span class="sr-only">{label}</span>
|
|
40
|
-
<svg
|
|
41
|
-
viewBox="0 0 16 16"
|
|
42
|
-
width="16"
|
|
43
|
-
height="16"
|
|
44
|
-
aria-hidden="true"
|
|
45
|
-
class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
|
|
46
|
-
fill="currentColor"
|
|
47
|
-
>
|
|
48
|
-
<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.5 4.5 0 1 0-9 0 4.5 4.5 0 0 0 9 0Z" />
|
|
49
|
-
</svg>
|
|
50
|
-
<input
|
|
51
|
-
id={id}
|
|
52
|
-
type="search"
|
|
53
|
-
name={name}
|
|
54
|
-
value={value}
|
|
55
|
-
placeholder={placeholder}
|
|
56
|
-
autocomplete="off"
|
|
57
|
-
data-search-input
|
|
58
|
-
class="peer h-10 w-full rounded-md border border-input bg-background py-2 pl-9 pr-10 text-sm text-foreground placeholder:text-muted-foreground focus:border-ring focus:outline-none focus:ring-2 focus:ring-ring/20 [&::-webkit-search-cancel-button]:appearance-none"
|
|
59
|
-
/>
|
|
60
|
-
{shortcut && (
|
|
61
|
-
<kbd
|
|
62
|
-
data-search-hint
|
|
63
|
-
aria-hidden="true"
|
|
64
|
-
class="pointer-events-none absolute right-2 top-1/2 hidden h-6 w-6 -translate-y-1/2 select-none items-center justify-center rounded-md border border-border bg-secondary font-mono text-[11px] leading-none text-muted-foreground peer-focus:!hidden peer-not-placeholder-shown:hidden sm:inline-flex"
|
|
65
|
-
>/</kbd
|
|
66
|
-
>
|
|
67
|
-
)}
|
|
68
|
-
<button
|
|
69
|
-
type="button"
|
|
70
|
-
data-search-clear
|
|
71
|
-
hidden={!value}
|
|
72
|
-
aria-label="Clear search"
|
|
73
|
-
class="absolute right-2 top-1/2 inline-flex h-6 w-6 -translate-y-1/2 cursor-pointer items-center justify-center rounded-full border-0 bg-transparent p-0 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/35"
|
|
74
|
-
>
|
|
75
|
-
<svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true" fill="currentColor">
|
|
76
|
-
<path
|
|
77
|
-
d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 1 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 0 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"
|
|
78
|
-
/>
|
|
79
|
-
</svg>
|
|
80
|
-
</button>
|
|
81
|
-
</label>
|
|
82
|
-
|
|
83
|
-
{shortcut && (
|
|
84
|
-
<script>
|
|
85
|
-
// `/` focuses the directory search from anywhere on the page, but
|
|
86
|
-
// never while the visitor is already typing somewhere — otherwise
|
|
87
|
-
// the shortcut eats a literal slash out of another field.
|
|
88
|
-
document.addEventListener("keydown", (event) => {
|
|
89
|
-
if (event.key !== "/" || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
90
|
-
const active = document.activeElement as HTMLElement | null;
|
|
91
|
-
if (
|
|
92
|
-
active &&
|
|
93
|
-
(active.tagName === "INPUT" ||
|
|
94
|
-
active.tagName === "TEXTAREA" ||
|
|
95
|
-
active.tagName === "SELECT" ||
|
|
96
|
-
active.isContentEditable)
|
|
97
|
-
)
|
|
98
|
-
return;
|
|
99
|
-
const input = document.querySelector("[data-search-input]");
|
|
100
|
-
if (!(input instanceof HTMLInputElement)) return;
|
|
101
|
-
event.preventDefault();
|
|
102
|
-
input.focus();
|
|
103
|
-
input.select();
|
|
104
|
-
});
|
|
105
|
-
</script>
|
|
106
|
-
)}
|
package/src/ui/button.test.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
import { buttonClass, chipClass, filterTriggerClass, lensTabClass } from "./button.js";
|
|
5
|
-
|
|
6
|
-
describe("button class builders", () => {
|
|
7
|
-
it("keeps selection state on the selected tokens, never the brand primary", () => {
|
|
8
|
-
for (const cls of [
|
|
9
|
-
buttonClass("selected"),
|
|
10
|
-
filterTriggerClass(true),
|
|
11
|
-
lensTabClass(true),
|
|
12
|
-
]) {
|
|
13
|
-
expect(cls).toContain("bg-selected");
|
|
14
|
-
expect(cls).not.toContain("bg-primary");
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("uses the brand primary only for the primary variant", () => {
|
|
19
|
-
expect(buttonClass("primary")).toContain("bg-primary");
|
|
20
|
-
expect(buttonClass("secondary")).not.toContain("bg-primary");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("guarantees a 44px mobile hit target on md and lg sizes", () => {
|
|
24
|
-
expect(buttonClass("secondary", "md")).toContain("max-sm:min-h-11");
|
|
25
|
-
expect(buttonClass("secondary", "lg")).toContain("h-11");
|
|
26
|
-
expect(filterTriggerClass(false)).toContain("max-sm:min-h-11");
|
|
27
|
-
expect(lensTabClass(false)).toContain("max-sm:min-h-11");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("keeps the filter-trigger hook class through client reassignment", () => {
|
|
31
|
-
expect(filterTriggerClass(true)).toContain("grove-filter-trigger");
|
|
32
|
-
expect(filterTriggerClass(false)).toContain("grove-filter-trigger");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("is the single source for chip classes on both server and client", () => {
|
|
36
|
-
// The browse page (server) and DirectoryIndexClient (client JS)
|
|
37
|
-
// must render byte-identical chips. Both now import chipClass —
|
|
38
|
-
// assert neither has regressed to an inline copy of the string.
|
|
39
|
-
const clientSource = readFileSync(
|
|
40
|
-
resolve(import.meta.dirname, "../components/DirectoryIndexClient.astro"),
|
|
41
|
-
"utf8",
|
|
42
|
-
);
|
|
43
|
-
const serverPage = readFileSync(
|
|
44
|
-
resolve(import.meta.dirname, "../../../../apps/example/src/components/DirectoryBrowse.astro"),
|
|
45
|
-
"utf8",
|
|
46
|
-
);
|
|
47
|
-
expect(clientSource).toContain("chipClass()");
|
|
48
|
-
expect(serverPage).toContain("chipClass()");
|
|
49
|
-
const inlineChip = "min-h-5 items-center justify-center gap-1 rounded-full";
|
|
50
|
-
expect(clientSource).not.toContain(inlineChip);
|
|
51
|
-
expect(serverPage).not.toContain(inlineChip);
|
|
52
|
-
expect(chipClass()).toContain(inlineChip);
|
|
53
|
-
});
|
|
54
|
-
});
|
package/src/ui/button.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Button / chip class builders — the single source for interactive
|
|
3
|
-
* control styling.
|
|
4
|
-
*
|
|
5
|
-
* Plain TS (not an .astro component) on purpose: `DirectoryIndexClient`
|
|
6
|
-
* rebuilds pagination links and active-filter chips in CLIENT-side JS,
|
|
7
|
-
* and those class strings must stay byte-identical with the server
|
|
8
|
-
* render. Both sides import these builders, so they cannot drift.
|
|
9
|
-
*
|
|
10
|
-
* Variant semantics (see styles.css):
|
|
11
|
-
* - `primary` — the brand action (config `theme.primaryColor`).
|
|
12
|
-
* - `secondary` — default chrome buttons.
|
|
13
|
-
* - `outline` / `ghost` — quieter chrome.
|
|
14
|
-
* - `selected` — "you are here" state (active filter, current lens,
|
|
15
|
-
* current page). Neutral inversion via the `selected` tokens —
|
|
16
|
-
* deliberately NOT the brand primary, so green never doubles as a
|
|
17
|
-
* selection indicator.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
export type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "selected";
|
|
21
|
-
/** `bare` emits no size classes — pass custom geometry via `extra`. */
|
|
22
|
-
export type ButtonSize = "sm" | "md" | "lg" | "bare";
|
|
23
|
-
|
|
24
|
-
const BASE =
|
|
25
|
-
"inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-transparent font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35";
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* `md`/`lg` guarantee a ≥44px hit target on small viewports
|
|
29
|
-
* (`max-sm:min-h-11`) while keeping the compact desktop height.
|
|
30
|
-
*/
|
|
31
|
-
const SIZES: Record<ButtonSize, string> = {
|
|
32
|
-
sm: "h-8 px-3 text-sm",
|
|
33
|
-
md: "h-9 px-3 text-sm max-sm:min-h-11",
|
|
34
|
-
lg: "h-11 px-4 text-sm",
|
|
35
|
-
bare: "",
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const VARIANTS: Record<ButtonVariant, string> = {
|
|
39
|
-
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
40
|
-
secondary: "border-border bg-secondary text-foreground hover:bg-accent",
|
|
41
|
-
outline: "border-border bg-transparent text-foreground hover:bg-accent",
|
|
42
|
-
ghost: "text-foreground hover:bg-accent",
|
|
43
|
-
selected: "bg-selected text-selected-foreground hover:bg-selected/90",
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export function buttonClass(
|
|
47
|
-
variant: ButtonVariant = "secondary",
|
|
48
|
-
size: ButtonSize = "sm",
|
|
49
|
-
extra = "",
|
|
50
|
-
): string {
|
|
51
|
-
return [BASE, SIZES[size], VARIANTS[variant], extra].filter(Boolean).join(" ");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Layout extras for pagination controls — shared by `Pagination.astro`
|
|
56
|
-
* and the client-side pagination rebuild in `DirectoryIndexClient`.
|
|
57
|
-
*/
|
|
58
|
-
export const PAGINATION_EXTRA = "min-w-9 px-2 py-1.5 text-2xs";
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* The dead end of a pagination row — "Previous" on page 1.
|
|
62
|
-
*
|
|
63
|
-
* Dimming to `opacity-40` put it at 3.37:1 against the dark surface,
|
|
64
|
-
* which is a contrast failure, not a disabled state. 60% still reads
|
|
65
|
-
* as unavailable and clears 4.5:1.
|
|
66
|
-
*/
|
|
67
|
-
export const PAGINATION_DISABLED = "pointer-events-none opacity-60";
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Facet filter trigger (the "Stack: Any" dropdown buttons). Includes
|
|
71
|
-
* the `grove-filter-trigger` hook class because the client script
|
|
72
|
-
* reassigns the whole className when the active state changes — the
|
|
73
|
-
* hook must survive that reassignment.
|
|
74
|
-
*/
|
|
75
|
-
export function filterTriggerClass(active: boolean): string {
|
|
76
|
-
return [
|
|
77
|
-
"grove-filter-trigger",
|
|
78
|
-
BASE,
|
|
79
|
-
"h-9 px-3 text-[0.8125rem] max-sm:min-h-11",
|
|
80
|
-
active ? `${VARIANTS.selected} hover:text-selected-foreground` : VARIANTS.secondary,
|
|
81
|
-
].join(" ");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Lens tab (the All / Hot / Mature row). Same geometry as the filter
|
|
86
|
-
* trigger; `data-lens-id` targeting keeps its own hook attribute.
|
|
87
|
-
*/
|
|
88
|
-
export function lensTabClass(active: boolean): string {
|
|
89
|
-
return [
|
|
90
|
-
BASE,
|
|
91
|
-
"h-9 px-3 text-[0.8125rem] max-sm:min-h-11",
|
|
92
|
-
active ? `${VARIANTS.selected} hover:text-selected-foreground` : VARIANTS.secondary,
|
|
93
|
-
].join(" ");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Active-filter chip (the removable "Stack: Python ×" pills). One
|
|
98
|
-
* definition for the server render and the client rebuild.
|
|
99
|
-
*/
|
|
100
|
-
export function chipClass(extra = ""): string {
|
|
101
|
-
return [
|
|
102
|
-
"inline-flex min-h-5 items-center justify-center gap-1 rounded-full border border-transparent bg-secondary px-2 py-0.5 align-middle text-xs font-medium leading-none text-secondary-foreground transition-colors hover:border-ink-400",
|
|
103
|
-
extra,
|
|
104
|
-
]
|
|
105
|
-
.filter(Boolean)
|
|
106
|
-
.join(" ");
|
|
107
|
-
}
|