@grove-dev/astro 0.2.16 → 0.2.19

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 (38) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +44 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/display.js +4 -4
  6. package/dist/lib/display.js.map +1 -1
  7. package/dist/lib/repo.d.ts.map +1 -1
  8. package/dist/lib/repo.js +3 -4
  9. package/dist/lib/repo.js.map +1 -1
  10. package/dist/lib/search.d.ts +2 -2
  11. package/dist/lib/search.d.ts.map +1 -1
  12. package/dist/lib/search.js +17 -6
  13. package/dist/lib/search.js.map +1 -1
  14. package/dist/theme.test.d.ts +2 -0
  15. package/dist/theme.test.d.ts.map +1 -0
  16. package/dist/theme.test.js +41 -0
  17. package/dist/theme.test.js.map +1 -0
  18. package/package.json +1 -1
  19. package/src/components/FilterGroupMenu.astro +3 -1
  20. package/src/components/Hero.astro +3 -3
  21. package/src/components/Icon.astro +4 -4
  22. package/src/components/IndexRow.astro +15 -2
  23. package/src/index.ts +52 -1
  24. package/src/layouts/BaseLayout.astro +12 -11
  25. package/src/layouts/Seo.astro +12 -4
  26. package/src/lib/display.ts +4 -4
  27. package/src/lib/repo.ts +3 -4
  28. package/src/lib/search.ts +18 -8
  29. package/src/styles.css +184 -69
  30. package/src/theme.test.ts +52 -0
  31. package/templates/default/package.json +2 -1
  32. package/templates/default/public/llms-full.txt +1 -1
  33. package/templates/default/public/sitemap.xml +10 -10
  34. package/templates/default/src/data/records.ts +115 -77
  35. package/templates/default/src/pages/[slug]/[recordSlug].astro +29 -33
  36. package/templates/default/src/pages/[slug]/index.astro +20 -296
  37. package/templates/default/src/styles/global.css +14 -39
  38. package/templates/default/tailwind.config.mjs +0 -130
@@ -7,12 +7,17 @@
7
7
  * - RefinePanel (Stack / Platform / Category / License / Sort)
8
8
  * - RecordSection rows (full record cards; the `ItemSection` V0
9
9
  * name was renamed to `RecordSection` in V1)
10
- * - ExploreByCategory / ExploreByStack (compact pill lists)
11
10
  *
12
11
  * Generic: the URL slug (e.g. `/projects/`, `/resources/`,
13
12
  * `/entities/`) is derived from the blueprint via `getStaticPaths`,
14
13
  * so the same page works for every blueprint without per-blueprint
15
14
  * forks.
15
+ *
16
+ * Navigation model: full-page render. The form posts via `method="get"`
17
+ * so the browser navigates to `?q=...&sort=...&stack=...`, and the
18
+ * server re-runs `filterRecords` / `applySort` / `paginate` from
19
+ * those query params. No client-side filter shim, no duplicated
20
+ * logic. The URL stays the single source of truth for filter state.
16
21
  */
17
22
  import siteConfig from "../../../data/generated/site-config.json";
18
23
  import { items, itemLabel, itemLabelPlural } from "../../data/records";
@@ -22,36 +27,27 @@ import SmartLensTabs from "@grove-dev/astro/components/SmartLensTabs.astro";
22
27
  import RefinePanel from "@grove-dev/astro/components/RefinePanel.astro";
23
28
  import IndexRow from "@grove-dev/astro/components/IndexRow.astro";
24
29
  import Pagination from "@grove-dev/astro/components/Pagination.astro";
25
- import ExploreByCategory from "@grove-dev/astro/components/ExploreByCategory.astro";
26
- import ExploreByStack from "@grove-dev/astro/components/ExploreByStack.astro";
27
30
  import {
28
31
  activeFilterChips,
29
32
  applySort,
30
33
  buildFacets,
31
- countByCategory,
32
- countByStack,
33
34
  effectivePage,
34
35
  effectiveSort,
35
36
  filterRecords,
36
37
  filtersFromSearchParams,
37
- paginate,
38
38
  searchParamsFromFilters,
39
39
  sortDisplay,
40
40
  SORT_OPTIONS,
41
41
  totalPages,
42
42
  } from "@grove-dev/astro";
43
43
 
44
- export async function getStaticPaths() {
45
- let activeSlug = "projects";
46
- try {
47
- const raw = await import("../../../data/generated/site-config.json");
48
- activeSlug = raw.default?.blueprintConfig?.routeSlug ?? "projects";
49
- } catch {
50
- /* not present */
51
- }
52
- const all = ["projects", "resources", "entities"];
53
- if (!all.includes(activeSlug)) all.push(activeSlug);
54
- return all.map((slug) => ({ params: { slug } }));
44
+ // This template ships with the `project-directory` blueprint, so the
45
+ // only directory slug it ever needs is `projects`. While it ships
46
+ // as the project directory, pre-generate the single path the build
47
+ // actually needs. Swapping in a different blueprint is a one-config
48
+ // change that flows through the static import at the top.
49
+ export function getStaticPaths() {
50
+ return [{ params: { slug: "projects" } }];
55
51
  }
56
52
 
57
53
  const slug = Astro.params.slug as string;
@@ -64,50 +60,11 @@ const filtered = filterRecords(items, filters);
64
60
  const sorted = applySort(filtered, sort);
65
61
  const pages = totalPages(sorted.length);
66
62
  const page = Math.min(requestedPage, pages);
67
- const visibleItems = paginate(sorted, page);
68
63
  const chips = activeFilterChips(filters);
69
- const clientItems = items.map((item) => ({
70
- slug: item.slug,
71
- name: item.kind === "resource" ? item.title : item.name,
72
- description: item.description,
73
- category: item.category,
74
- tags: item.tags,
75
- labels: item.curation?.labels ?? [],
76
- lenses: item.curation?.lenses ?? [],
77
- reviewed: Boolean(item.curation?.reviewed),
78
- addedAt: item.curation?.reviewedAt ?? "",
79
- stacks: item.kind === "project" ? [item.stack, ...item.stacks].filter(Boolean) : [],
80
- platforms: item.kind === "project" ? item.platforms : [],
81
- license: item.kind === "project" ? item.github?.license ?? "" : "",
82
- status: item.kind === "project" ? item.health?.status ?? "" : "",
83
- stars: item.kind === "project" ? item.github?.stars ?? 0 : 0,
84
- updatedAt:
85
- item.kind === "project"
86
- ? item.github?.pushedAt ?? ""
87
- : item.kind === "resource"
88
- ? item.publishedAt ?? ""
89
- : "",
90
- extra:
91
- item.kind === "project"
92
- ? [item.repoUrl, item.projectType, item.difficulty, item.codebaseSize, ...item.bestFor, ...item.whyListed]
93
- : item.kind === "resource"
94
- ? [item.type, item.topic, item.author ?? ""]
95
- : [item.type, item.location ?? "", item.parent ?? ""],
96
- }));
97
64
 
98
65
  const title = `Browse ${slug} — ${siteConfig.name}`;
99
66
  const description = `A searchable directory of ${itemLabelPlural()} for ${siteConfig.name}.`;
100
67
 
101
- // Compute the top categories and stacks for the explore pills
102
- const categoryEntries = [...countByCategory(items as Parameters<typeof countByCategory>[0]).entries()]
103
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
104
- .slice(0, 12)
105
- .map(([name, count]) => ({ name, slug: name, count }));
106
- const stackEntries = [...countByStack(items as Parameters<typeof countByStack>[0]).entries()]
107
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
108
- .slice(0, 18)
109
- .map(([name, count]) => ({ name, slug: name, count }));
110
-
111
68
  function hrefForPage(target: number): string {
112
69
  const next: IndexFilters = { ...filters, page: target };
113
70
  const query = searchParamsFromFilters(next).toString();
@@ -235,20 +192,6 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
235
192
  </div>
236
193
  </section>
237
194
 
238
- {categoryEntries.length > 0 && (
239
- <ExploreByCategory
240
- categories={categoryEntries}
241
- pathPrefix={`/${slug}`}
242
- />
243
- )}
244
-
245
- {stackEntries.length > 0 && (
246
- <ExploreByStack
247
- stacks={stackEntries}
248
- pathPrefix={`/${slug}`}
249
- />
250
- )}
251
-
252
195
  <section class="grove-section-tight">
253
196
  <div class="grove-container-wide mx-auto px-5 sm:px-7">
254
197
  <header class="mb-6">
@@ -291,9 +234,13 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
291
234
  </div>
292
235
 
293
236
  <ul id="results-list" class="m-0 flex list-none flex-col gap-3 p-0">
294
- {sorted.map((item) => (
237
+ {sorted.map((item, i) => (
295
238
  <li class="m-0 p-0" data-record-slug={item.slug}>
296
- <IndexRow record={item as Parameters<typeof IndexRow>[0]["record"]} detailHref={`/${slug}/${item.slug}`} />
239
+ <IndexRow
240
+ record={item as Parameters<typeof IndexRow>[0]["record"]}
241
+ detailHref={`/${slug}/${item.slug}`}
242
+ eager={i < 2}
243
+ />
297
244
  </li>
298
245
  ))}
299
246
  </ul>
@@ -324,231 +271,8 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
324
271
  >
325
272
  {`Submit ${itemLabel()}`}
326
273
  </a>
274
+
327
275
  </div>
328
276
  </div>
329
277
  </section>
330
-
331
- <script is:inline define:vars={{ clientItems, slug, itemSingular: itemLabel(), itemPlural: itemLabelPlural() }}>
332
- (function () {
333
- var PAGE_SIZE = 20;
334
- var list = document.getElementById("results-list");
335
- if (!list) return;
336
-
337
- // Sort dropdown: navigate on change so the server re-renders
338
- // the sorted list (URL is the source of truth for filters/sort).
339
- var sortSelect = document.getElementById("sort-select");
340
- if (sortSelect) {
341
- sortSelect.addEventListener("change", function () {
342
- var params = new URLSearchParams(location.search);
343
- if (sortSelect.value && sortSelect.value !== "recently-updated") {
344
- params.set("sort", sortSelect.value);
345
- } else {
346
- params.delete("sort");
347
- }
348
- params.delete("page");
349
- var qs = params.toString();
350
- location.assign(qs ? "?" + qs : location.pathname);
351
- });
352
- }
353
-
354
- // Search: 250ms debounce; pressing Enter still submits
355
- // immediately via the form's normal submit handler.
356
- var searchInput = document.getElementById("search-input");
357
- var searchForm = document.getElementById("search-form");
358
- if (searchInput && searchForm) {
359
- var debounceTimer = null;
360
- var submit = function () {
361
- var params = new URLSearchParams(location.search);
362
- var term = searchInput.value.trim();
363
- if (term) params.set("q", term);
364
- else params.delete("q");
365
- params.delete("page");
366
- var qs = params.toString();
367
- location.assign(qs ? "?" + qs : location.pathname);
368
- };
369
- searchInput.addEventListener("input", function () {
370
- if (debounceTimer) window.clearTimeout(debounceTimer);
371
- debounceTimer = window.setTimeout(submit, 250);
372
- });
373
- searchForm.addEventListener("submit", function (event) {
374
- event.preventDefault();
375
- if (debounceTimer) window.clearTimeout(debounceTimer);
376
- submit();
377
- });
378
- }
379
-
380
- function values(params, key) {
381
- return params.getAll(key).filter(Boolean);
382
- }
383
-
384
- function display(value) {
385
- return String(value || "")
386
- .split(/[-_]+/)
387
- .filter(Boolean)
388
- .map(function (part) { return part.charAt(0).toUpperCase() + part.slice(1); })
389
- .join(" ");
390
- }
391
-
392
- function sortLabel(value) {
393
- return {
394
- "recently-updated": "Recently updated",
395
- "most-starred": "Most starred",
396
- "recently-added": "Recently added",
397
- "best-overall": "Best overall",
398
- alphabetical: "Alphabetical",
399
- }[value] || "Recently updated";
400
- }
401
-
402
- function pageHref(params, page) {
403
- var next = new URLSearchParams(params);
404
- if (page <= 1) next.delete("page");
405
- else next.set("page", String(page));
406
- var query = next.toString();
407
- return query ? "/" + slug + "?" + query : "/" + slug;
408
- }
409
-
410
- function apply() {
411
- var params = new URLSearchParams(location.search);
412
- var query = (params.get("q") || "").trim().toLowerCase();
413
- var stacks = values(params, "stack");
414
- var platforms = values(params, "platform");
415
- var categories = values(params, "category");
416
- var labels = values(params, "label");
417
- var licenses = values(params, "license");
418
- var statuses = (params.get("status") || "").split(",").filter(Boolean);
419
- var lens = params.get("lens") || "";
420
- var sort = params.get("sort") || "recently-updated";
421
- var page = Math.max(1, Number(params.get("page")) || 1);
422
-
423
- var filtered = clientItems.filter(function (item) {
424
- var haystack = [
425
- item.name,
426
- item.description,
427
- item.category,
428
- item.tags,
429
- item.stacks,
430
- item.platforms,
431
- item.license,
432
- item.status,
433
- item.extra,
434
- ].flat(3).join(" ").toLowerCase();
435
- if (query && !haystack.includes(query)) return false;
436
- if (stacks.length && !item.stacks.some(function (value) { return stacks.includes(value); })) return false;
437
- if (platforms.length && !item.platforms.some(function (value) { return platforms.includes(value); })) return false;
438
- if (categories.length && !categories.includes(item.category)) return false;
439
- if (labels.length && !item.labels.some(function (value) { return labels.includes(value); })) return false;
440
- if (licenses.length && !licenses.includes(item.license)) return false;
441
- if (statuses.length && !statuses.includes(item.status)) return false;
442
- if (lens && !item.lenses.includes(lens)) return false;
443
- return true;
444
- });
445
-
446
- filtered.sort(function (a, b) {
447
- if (sort === "most-starred") return b.stars - a.stars;
448
- if (sort === "recently-added") return Date.parse(b.addedAt || 0) - Date.parse(a.addedAt || 0);
449
- if (sort === "best-overall") return Number(b.reviewed) - Number(a.reviewed) || b.stars - a.stars;
450
- if (sort === "alphabetical") return a.name.localeCompare(b.name);
451
- return Date.parse(b.updatedAt || 0) - Date.parse(a.updatedAt || 0);
452
- });
453
-
454
- var totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
455
- page = Math.min(page, totalPages);
456
- var visible = new Set(
457
- filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE).map(function (item) { return item.slug; }),
458
- );
459
- var order = new Map(filtered.map(function (item, index) { return [item.slug, index]; }));
460
- Array.from(list.children)
461
- .sort(function (a, b) {
462
- return (order.get(a.dataset.recordSlug) ?? Number.MAX_SAFE_INTEGER) -
463
- (order.get(b.dataset.recordSlug) ?? Number.MAX_SAFE_INTEGER);
464
- })
465
- .forEach(function (node) {
466
- node.hidden = !visible.has(node.dataset.recordSlug);
467
- list.appendChild(node);
468
- });
469
-
470
- var count = document.getElementById("results-count");
471
- if (count) count.textContent = filtered.length + " " + (filtered.length === 1 ? itemSingular : itemPlural);
472
- var sortText = document.getElementById("results-sort");
473
- if (sortText) sortText.textContent = " · sorted by " + sortLabel(sort);
474
- var pageText = document.getElementById("results-page");
475
- if (pageText) pageText.textContent = totalPages > 1 ? " · page " + page + " of " + totalPages : "";
476
- document.getElementById("empty-state")?.classList.toggle("hidden", filtered.length > 0);
477
-
478
- var search = document.querySelector('input[name="q"]');
479
- if (search) search.value = params.get("q") || "";
480
- document.querySelectorAll("select").forEach(function (select) {
481
- var selected = values(params, select.name);
482
- if (select.name === "sort") selected = [sort];
483
- Array.from(select.options).forEach(function (option) {
484
- option.selected = selected.includes(option.value);
485
- });
486
- });
487
-
488
- // Pagination is server-rendered (see <Pagination /> in the
489
- // template) so the static HTML contains the page-number
490
- // links for SEO. No client-side rendering loop here.
491
-
492
- var chipHost = document.getElementById("active-filters");
493
- if (chipHost) {
494
- chipHost.innerHTML = "";
495
- var specs = [
496
- ["q", params.get("q") ? "“" + params.get("q") + "”" : ""],
497
- ...stacks.map(function (v) { return ["stack", "Stack: " + v]; }),
498
- ...platforms.map(function (v) { return ["platform", "Platform: " + v]; }),
499
- ...categories.map(function (v) { return ["category", "Category: " + v]; }),
500
- ...labels.map(function (v) { return ["label", display(v)]; }),
501
- ...licenses.map(function (v) { return ["license", "License: " + v]; }),
502
- ...(lens ? [["lens", display(lens)]] : []),
503
- ...statuses.map(function (v) { return ["status", display(v)]; }),
504
- ].filter(function (entry) { return entry[1]; });
505
- specs.forEach(function (entry) {
506
- var key = entry[0];
507
- var chip = document.createElement("a");
508
- var next = new URLSearchParams(params);
509
- if (key === "status") {
510
- var remaining = statuses.filter(function (value) { return display(value) !== entry[1]; });
511
- next.delete("status");
512
- if (remaining.length) next.set("status", remaining.join(","));
513
- } else {
514
- next.delete(key);
515
- }
516
- next.delete("page");
517
- chip.href = next.toString() ? "/" + slug + "?" + next : "/" + slug;
518
- chip.className = "grove-badge transition-colors hover:border-ink-400";
519
- chip.textContent = entry[1] + " x";
520
- chipHost.appendChild(chip);
521
- });
522
- if (specs.length) {
523
- var clear = document.createElement("a");
524
- clear.href = "/" + slug;
525
- clear.className = "text-2xs font-medium text-ink-500 hover:text-ink-900";
526
- clear.textContent = "Clear all";
527
- chipHost.appendChild(clear);
528
- }
529
- chipHost.classList.toggle("hidden", specs.length === 0);
530
- }
531
- }
532
-
533
- document.querySelectorAll("form").forEach(function (form) {
534
- form.addEventListener("submit", function (event) {
535
- if (!form.closest("main")) return;
536
- event.preventDefault();
537
- var params = new URLSearchParams(location.search);
538
- form.querySelectorAll("input, select").forEach(function (field) {
539
- if (!field.name) return;
540
- params.delete(field.name);
541
- if (field instanceof HTMLSelectElement && field.multiple) {
542
- Array.from(field.selectedOptions).forEach(function (option) { params.append(field.name, option.value); });
543
- } else if (field.value) {
544
- params.set(field.name, field.value);
545
- }
546
- });
547
- params.delete("page");
548
- location.assign(params.toString() ? "?" + params : location.pathname);
549
- });
550
- });
551
- apply();
552
- })();
553
- </script>
554
278
  </BaseLayout>
@@ -5,26 +5,22 @@
5
5
  * at scaffold time; we expose the same names as CSS variables so
6
6
  * consumers can layer their own brand colors in without forking.
7
7
  *
8
- * The `ink-*` color scale is preserved from the V0 template because
9
- * the page-level classes (`text-ink-900`, `bg-ink-50`, ...) still
10
- * reference it. New pages should prefer the default Tailwind
11
- * `gray-*` / `slate-*` palettes; the ink scale exists for
12
- * back-compat with the V1 minimal template.
8
+ * The package owns the Starlight-aligned palette, semantic theme tokens,
9
+ * font stacks, and light/dark base styles. This consumer sheet contains
10
+ * only scaffold-level overrides so those foundations cannot drift when
11
+ * the package theme changes.
12
+ *
13
+ * NOTE: do NOT add `@custom-variant dark` here. `@grove-dev/astro`'s
14
+ * `styles.css` already declares the `.dark`-class variant, and
15
+ * Tailwind v4 only honors one definition per build — adding a
16
+ * second one in this consumer file has been observed to confuse
17
+ * the variant resolution and produce utilities that key off
18
+ * `prefers-color-scheme` instead of the `.dark` class on <html>.
19
+ * The net effect: `<html class="dark">` is set but the page stays
20
+ * in light mode because the `dark:` utilities never match.
13
21
  */
14
22
  @theme {
15
- --color-ink-50: #fafafa;
16
- --color-ink-100: #f4f4f5;
17
- --color-ink-200: #e4e4e7;
18
- --color-ink-300: #d4d4d8;
19
- --color-ink-400: #a1a1aa;
20
- --color-ink-500: #71717a;
21
- --color-ink-600: #52525b;
22
- --color-ink-700: #3f3f46;
23
- --color-ink-800: #27272a;
24
- --color-ink-900: #18181b;
25
- --color-ink-950: #09090b;
26
-
27
- --color-grove-primary: #16a34a;
23
+ --color-grove-primary: var(--color-foreground);
28
24
  }
29
25
 
30
26
  @layer base {
@@ -33,26 +29,5 @@
33
29
  --grove-radius: 0.5rem;
34
30
  --grove-container-width: 72rem;
35
31
 
36
- color-scheme: light dark;
37
- }
38
-
39
- html {
40
- -webkit-text-size-adjust: 100%;
41
- -webkit-font-smoothing: antialiased;
42
- -moz-osx-font-smoothing: grayscale;
43
- }
44
-
45
- body {
46
- background: #ffffff;
47
- color: var(--color-ink-900);
48
- font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
49
- "Helvetica Neue", Arial, sans-serif;
50
- font-size: 15px;
51
- line-height: 1.55;
52
- }
53
-
54
- body.dark {
55
- background: var(--color-ink-950);
56
- color: var(--color-ink-100);
57
32
  }
58
33
  }
@@ -1,130 +0,0 @@
1
- /** @type {import('tailwindcss').Config} */
2
- export default {
3
- content: ['./src/**/*.{astro,html,js,jsx,ts,tsx,md,mdx}'],
4
- // Use a `.dark` class on <html> (toggled by the theme script in
5
- // the document head) rather than the OS media query, so a manual
6
- // theme switcher can override the user's system preference.
7
- darkMode: 'class',
8
- theme: {
9
- // Disable default palette so we only ship our own neutral / accent
10
- // tokens. Keeps the bundle small and forces a unified look.
11
- colors: {
12
- transparent: 'transparent',
13
- current: 'currentColor',
14
- white: '#ffffff',
15
- black: '#000000',
16
- ink: {
17
- 50: '#fafafa',
18
- 100: '#f4f4f5',
19
- 200: '#e4e4e7',
20
- 300: '#d4d4d8',
21
- 400: '#a1a1aa',
22
- 500: '#71717a',
23
- 600: '#52525b',
24
- 700: '#3f3f46',
25
- 800: '#27272a',
26
- 900: '#18181b',
27
- 950: '#09090b',
28
- },
29
- accent: {
30
- DEFAULT: '#1f6feb',
31
- muted: '#388bfd1a',
32
- },
33
- emerald: {
34
- 50: '#ecfdf5',
35
- 300: '#6ee7b7',
36
- 400: '#34d399',
37
- 700: '#047857',
38
- 800: '#065f46',
39
- 950: '#022c22',
40
- },
41
- orange: {
42
- 50: '#fff7ed',
43
- 300: '#fdba74',
44
- 400: '#fb923c',
45
- 700: '#c2410c',
46
- 800: '#9a3412',
47
- 950: '#431407',
48
- },
49
- blue: {
50
- 50: '#eff6ff',
51
- 300: '#93c5fd',
52
- 400: '#60a5fa',
53
- 700: '#1d4ed8',
54
- 800: '#1e40af',
55
- 950: '#172554',
56
- },
57
- amber: {
58
- 50: '#fffbeb',
59
- 300: '#fcd34d',
60
- 400: '#fbbf24',
61
- 700: '#b45309',
62
- 800: '#92400e',
63
- 950: '#451a03',
64
- },
65
- },
66
- extend: {
67
- fontFamily: {
68
- sans: [
69
- 'ui-sans-serif',
70
- 'system-ui',
71
- '-apple-system',
72
- 'BlinkMacSystemFont',
73
- '"Segoe UI"',
74
- 'Roboto',
75
- '"Helvetica Neue"',
76
- 'Arial',
77
- 'sans-serif',
78
- ],
79
- mono: [
80
- 'ui-monospace',
81
- 'SFMono-Regular',
82
- 'Menlo',
83
- 'Monaco',
84
- 'Consolas',
85
- '"Liberation Mono"',
86
- '"Courier New"',
87
- 'monospace',
88
- ],
89
- },
90
- fontSize: {
91
- '2xs': ['0.6875rem', { lineHeight: '1rem' }],
92
- },
93
- maxWidth: {
94
- container: '1140px',
95
- wide: '1400px',
96
- narrow: '720px',
97
- },
98
- borderRadius: {
99
- DEFAULT: '0.375rem',
100
- lg: '0.5rem',
101
- xl: '0.75rem',
102
- },
103
- keyframes: {
104
- 'fade-in-up': {
105
- '0%': { opacity: '0', transform: 'translateY(8px)' },
106
- '100%': { opacity: '1', transform: 'translateY(0)' },
107
- },
108
- 'fade-in': {
109
- '0%': { opacity: '0' },
110
- '100%': { opacity: '1' },
111
- },
112
- 'gradient-shift': {
113
- '0%, 100%': { 'background-position': '0% 50%' },
114
- '50%': { 'background-position': '100% 50%' },
115
- },
116
- 'subtle-bob': {
117
- '0%, 100%': { transform: 'translateY(0)' },
118
- '50%': { transform: 'translateY(-2px)' },
119
- },
120
- },
121
- animation: {
122
- 'fade-in-up': 'fade-in-up 0.6s ease-out both',
123
- 'fade-in': 'fade-in 0.8s ease-out both',
124
- 'gradient-shift': 'gradient-shift 8s ease-in-out infinite',
125
- 'subtle-bob': 'subtle-bob 3s ease-in-out infinite',
126
- },
127
- },
128
- },
129
- plugins: [],
130
- };