@at-flux/astroflare 1.0.3 → 1.0.4

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 (40) hide show
  1. package/README.md +98 -14
  2. package/dist/chunk-pbuEa-1d.js +13 -0
  3. package/dist/core.cjs +162 -138
  4. package/dist/core.d.cts +59 -2
  5. package/dist/core.d.ts +59 -2
  6. package/dist/core.js +147 -16
  7. package/dist/forms/index.cjs +7 -130
  8. package/dist/forms/index.d.cts +2 -52
  9. package/dist/forms/index.d.ts +2 -52
  10. package/dist/forms/index.js +99 -14
  11. package/dist/{chunk-DRYHJSYC.js → forms-8uIdWmMD.cjs} +81 -53
  12. package/dist/index-Bk-K9aDQ.d.ts +44 -0
  13. package/dist/index-CcH_yXr-.d.cts +44 -0
  14. package/dist/index.cjs +20 -141
  15. package/dist/index.d.cts +3 -2
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.js +3 -17
  18. package/package.json +13 -7
  19. package/src/components/CollectionQuery.astro +107 -0
  20. package/src/components/ContactModalCta.astro +2 -0
  21. package/src/components/FilterPills.astro +164 -0
  22. package/src/components/IconButton.astro +5 -0
  23. package/src/components/InstagramProfileLink.astro +3 -0
  24. package/src/components/ListSummary.astro +63 -0
  25. package/src/components/MediaProtect.astro +41 -0
  26. package/src/components/Modal.astro +3 -0
  27. package/src/components/ModalTrigger.astro +2 -0
  28. package/src/components/Pager.astro +92 -0
  29. package/src/components/Section.astro +3 -0
  30. package/src/components/TagSummary.astro +71 -0
  31. package/src/components/ThemeToggle.astro +1 -0
  32. package/src/components/Tooltip.astro +70 -0
  33. package/src/components/collection-query-props.ts +45 -0
  34. package/src/runtime/collection-query.ts +156 -0
  35. package/src/runtime/media-protect.ts +70 -0
  36. package/src/styles/accessibility.css +1 -1
  37. package/src/styles/no-save.css +2 -2
  38. package/src/styles/prose.css +23 -11
  39. package/src/styles/scrollbar.css +10 -3
  40. package/dist/chunk-ALMPH3A2.js +0 -0
package/README.md CHANGED
@@ -1,32 +1,34 @@
1
1
  # @at-flux/astroflare
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@at-flux/astroflare.svg)](https://www.npmjs.com/package/@at-flux/astroflare)
4
+ [![CI](https://github.com/at-flux/astroflare/actions/workflows/ci.yml/badge.svg)](https://github.com/at-flux/astroflare/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
3
7
  Reusable headless components, styles, and utilities for Astro + Tailwind v4 + Cloudflare projects.
4
8
 
5
9
  For type-safe DOM helpers, use the separate package **`@at-flux/dom`**.
6
10
 
7
- **There is no `@at-flux/astroflare/dom` subpath** (it is not published and must not be used). Add `@at-flux/dom` as its own dependency.
8
-
9
11
  ## Package entrypoints
10
12
 
11
- | Subpath | Contents |
12
- |---------|----------|
13
- | `@at-flux/astroflare` | **Core** — forms utilities (same as `./core`) |
14
- | `@at-flux/astroflare/core` | Forms; also exposes the `forms` namespace |
15
- | `@at-flux/astroflare/forms` | Resend email + form HTML helpers |
16
- | `@at-flux/astroflare/components/*` | Astro components (source) |
17
- | `@at-flux/astroflare/styles/*` | CSS (source) |
13
+ | Subpath | Contents |
14
+ | ---------------------------------- | --------------------------------------------- |
15
+ | `@at-flux/astroflare` | **Core** — forms utilities (same as `./core`) |
16
+ | `@at-flux/astroflare/core` | Forms; also exposes the `forms` namespace |
17
+ | `@at-flux/astroflare/forms` | Resend email + form HTML helpers |
18
+ | `@at-flux/astroflare/components/*` | Astro components (source) |
19
+ | `@at-flux/astroflare/styles/*` | CSS (source) |
18
20
 
19
21
  ### Examples
20
22
 
21
23
  ```ts
22
24
  // Flat imports from core
23
- import { sendEmail } from '@at-flux/astroflare';
25
+ import { sendEmail } from "@at-flux/astroflare";
24
26
 
25
27
  // Explicit subpaths
26
- import { renderEmailTemplate } from '@at-flux/astroflare/forms';
28
+ import { renderEmailTemplate } from "@at-flux/astroflare/forms";
27
29
 
28
30
  // Namespaced (from core / root)
29
- import { forms } from '@at-flux/astroflare/core';
31
+ import { forms } from "@at-flux/astroflare/core";
30
32
  ```
31
33
 
32
34
  ## Contents
@@ -39,7 +41,75 @@ import { forms } from '@at-flux/astroflare/core';
39
41
  - `InstagramProfileLink.astro` — Small Instagram icon + `@handle` link with safe defaults
40
42
  - `Section.astro` — Page section with optional `narrow` and `contentOnly` (inner width wrapper without outer padding)
41
43
  - `ThemeToggle.astro` — Dark/light mode toggle using `<theme-toggle>` web component
44
+ - `IconButton.astro` — Accessible icon-only control that renders `<button>` or `<a>`
42
45
  - `ClientRouterLoadingSpinner.astro` — Loading spinner for Astro view transitions
46
+ - `Tooltip.astro` — Lightweight hover/focus tooltip wrapper for compact metadata summaries
47
+ - `ListSummary.astro` — Generic inline list truncation with `+N` overflow and tooltip details
48
+ - `TagSummary.astro` — Generic deterministic tag pill list with `+N` tooltip overflow
49
+ - `MediaProtect.astro` — Style-agnostic media wrapper that applies no-save classes with delegated drag/context-menu protection
50
+ - `FilterPills.astro` — Tag-colored filter chips with an `all` option and active-state styling
51
+ - `Pager.astro` — Pagination UI primitive for both browser-only and link-driven query pagination
52
+ - `CollectionQuery.astro` — Unified collection filtering/pagination component (client mode by default; URL-driven server mode with `useServer`)
53
+
54
+ #### Component props reference
55
+
56
+ - `Modal.astro`: `id`, `class`, `backdropClass`, `panelClass`, `closeButtonClass`, `contentClass`
57
+ - `ModalTrigger.astro`: `modalId`, `class`
58
+ - `ContactModalCta.astro`: `modalId`, `label`, `variant`, `class`
59
+ - `InstagramProfileLink.astro`: `handle`, `href`, `class`, `aria-label`
60
+ - `Section.astro`: `id`, `class`, `narrow`, `contentOnly`
61
+ - `ThemeToggle.astro`: `class`
62
+ - `IconButton.astro`: `label`, `href`, `class`, `id`, passthrough attributes
63
+ - `Tooltip.astro`: `text`, `position`, `class`, `panelClass`
64
+ - `ListSummary.astro`: `items`, `visibleCount`, `separator`, `class`, `emptyLabel`, `overflowClass`, `tooltipPosition`, `itemCase`
65
+ - `TagSummary.astro`: `items`, `visibleCount`, `class`, `itemClass`, `overflowClass`, `itemCase`, `colorOverrides`
66
+ - `MediaProtect.astro`: `class`, `containerClass`, `drag`, `contextMenu`
67
+ - `FilterPills.astro`: `items`, `includeAll`, `allLabel`, `allHref`, `active`, `itemCase`, `colorOverrides`, `class`
68
+ - `Pager.astro`: `pageCount`, `activePage`, `items`, `class`
69
+ - `CollectionQuery.astro`: `useServer`, `pathname`, `query`, `totalPages`, `currentPage`, `filters`, `maxPageButtons`, `filtersClass`, `pagerClass`, `perPage`, `class`
70
+ - when `useServer` is `true`, `pathname`, `query`, `totalPages`, and `currentPage` are required
71
+
72
+ ### Server Islands Pattern
73
+
74
+ `CollectionQuery.astro` supports two modes:
75
+
76
+ - client mode (default): static cards are filtered/paged in the browser
77
+ - server mode (`useServer`): renders querystring links for filters + pager
78
+
79
+ For server mode, mount with `server:defer` at the page callsite:
80
+
81
+ ```astro
82
+ <CollectionQuery
83
+ useServer
84
+ pathname={Astro.url.pathname}
85
+ query={activeQuery}
86
+ totalPages={pageData.totalPages}
87
+ currentPage={currentPage}
88
+ filters={filters}
89
+ server:defer
90
+ />
91
+ ```
92
+
93
+ The package styleguide uses the Node adapter, so the server-island pattern can be exercised there with `server:defer`.
94
+
95
+ ### Slot customization (headless overrides)
96
+
97
+ Use named slots to replace the default filter/pager rendering:
98
+
99
+ ```astro
100
+ <CollectionQuery useServer {...props}>
101
+ <div slot="filters">
102
+ <!-- your custom filter UI -->
103
+ </div>
104
+
105
+ <!-- default slot: your collection items -->
106
+ <div>...</div>
107
+
108
+ <div slot="pager">
109
+ <!-- your custom pager UI -->
110
+ </div>
111
+ </CollectionQuery>
112
+ ```
43
113
 
44
114
  ### Styles (CSS)
45
115
 
@@ -48,6 +118,12 @@ import { forms } from '@at-flux/astroflare/core';
48
118
  - `styles/accessibility.css` — Focus styles, reduced motion, selection styling
49
119
  - `styles/scrollbar.css` — Branded scrollbar styling
50
120
 
121
+ ### Utilities
122
+
123
+ - `getTagPalette(tag, options?)` — Deterministic, readable tag color assignment with optional explicit overrides
124
+ - `formatDisplayDate(date, config?)` — Consistent card/detail date formatting with locale override support
125
+ - `parseCollectionQuery(searchParams, options?)` + `paginateCollection(list, query)` + `buildCollectionHref(path, query, overrides?)` + `buildPageSequence(total, current, max?)` + `matchesCollectionFilters(values, filters)` — URL-driven filtering and pagination helpers (`filters` query param supports stringified JSON for multi-filter payloads)
126
+
51
127
  ## Usage
52
128
 
53
129
  ### Local checkout without changing package.json or lockfile
@@ -93,8 +169,8 @@ import ModalTrigger from '@at-flux/astroflare/components/ModalTrigger.astro';
93
169
  ### Styles
94
170
 
95
171
  ```css
96
- @import '@at-flux/astroflare/styles/prose.css';
97
- @import '@at-flux/astroflare/styles/no-save.css';
172
+ @import "@at-flux/astroflare/styles/prose.css";
173
+ @import "@at-flux/astroflare/styles/no-save.css";
98
174
  ```
99
175
 
100
176
  ## Testing
@@ -111,3 +187,11 @@ Or in `packages/astroflare`:
111
187
  ```bash
112
188
  pnpm test
113
189
  ```
190
+
191
+ ## Styleguide (dev-only)
192
+
193
+ Use the package-local styleguide to preview all astroflare components in one place.
194
+
195
+ ```bash
196
+ pnpm styleguide:dev
197
+ ```
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };
package/dist/core.cjs CHANGED
@@ -1,142 +1,166 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_forms = require("./forms-8uIdWmMD.cjs");
3
+ //#region src/tag-colors.ts
4
+ const DEFAULT_OVERRIDES = {
5
+ shoots: "#dc3545",
6
+ tech: "#8a95a5",
7
+ ai: "#6b7b8d",
8
+ sites: "#7dd3c0",
9
+ flow: "#b367e0"
9
10
  };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
11
+ const hashStringToHue = (input) => {
12
+ let hash = 0;
13
+ for (let i = 0; i < input.length; i += 1) hash = hash * 31 + input.charCodeAt(i) >>> 0;
14
+ return hash % 360;
17
15
  };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/core.ts
21
- var core_exports = {};
22
- __export(core_exports, {
23
- composeEmailAddress: () => composeEmailAddress,
24
- forms: () => forms_exports,
25
- generateFormResultHtml: () => generateFormResultHtml,
26
- generateFormSectionHtml: () => generateFormSectionHtml,
27
- renderEmailTemplate: () => renderEmailTemplate,
28
- sendEmail: () => sendEmail
29
- });
30
- module.exports = __toCommonJS(core_exports);
31
-
32
- // src/forms/index.ts
33
- var forms_exports = {};
34
- __export(forms_exports, {
35
- composeEmailAddress: () => composeEmailAddress,
36
- generateFormResultHtml: () => generateFormResultHtml,
37
- generateFormSectionHtml: () => generateFormSectionHtml,
38
- renderEmailTemplate: () => renderEmailTemplate,
39
- sendEmail: () => sendEmail
16
+ const hexToRgb = (hex) => {
17
+ const value = hex.replace("#", "").trim();
18
+ if (!/^[0-9a-fA-F]{6}$/.test(value)) return null;
19
+ return [
20
+ Number.parseInt(value.slice(0, 2), 16),
21
+ Number.parseInt(value.slice(2, 4), 16),
22
+ Number.parseInt(value.slice(4, 6), 16)
23
+ ];
24
+ };
25
+ const rgbToHsl = (r, g, b) => {
26
+ const red = r / 255;
27
+ const green = g / 255;
28
+ const blue = b / 255;
29
+ const max = Math.max(red, green, blue);
30
+ const min = Math.min(red, green, blue);
31
+ const delta = max - min;
32
+ const lightness = (max + min) / 2;
33
+ const saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * lightness - 1));
34
+ let hue = 0;
35
+ if (delta !== 0) if (max === red) hue = (green - blue) / delta % 6;
36
+ else if (max === green) hue = (blue - red) / delta + 2;
37
+ else hue = (red - green) / delta + 4;
38
+ return [
39
+ Math.round((hue * 60 + 360) % 360),
40
+ Math.round(saturation * 100),
41
+ Math.round(lightness * 100)
42
+ ];
43
+ };
44
+ const resolveHue = (tag, options) => {
45
+ const key = tag.trim().toLowerCase();
46
+ const color = options.overrides?.[key] ?? DEFAULT_OVERRIDES[key];
47
+ if (!color) return hashStringToHue(key);
48
+ const rgb = hexToRgb(color);
49
+ if (!rgb) return hashStringToHue(key);
50
+ return rgbToHsl(...rgb)[0];
51
+ };
52
+ /**
53
+ * Build a deterministic HSL-based palette for a tag label.
54
+ * Uses known defaults and optional overrides before falling back to hashed hue.
55
+ */
56
+ const getTagPalette = (tag, options = {}) => {
57
+ const hue = resolveHue(tag, options);
58
+ return {
59
+ bg: `hsla(${hue} 48% 58% / 0.12)`,
60
+ border: `hsla(${hue} 52% 58% / 0.34)`,
61
+ text: `hsl(${hue} 50% 66%)`
62
+ };
63
+ };
64
+ //#endregion
65
+ //#region src/date-format.ts
66
+ const DEFAULT_OPTIONS = {
67
+ year: "numeric",
68
+ month: "long",
69
+ day: "numeric"
70
+ };
71
+ /**
72
+ * Format dates consistently for cards and article metadata.
73
+ */
74
+ const formatDisplayDate = (date, config = {}) => {
75
+ const { locale = "en-GB", options = DEFAULT_OPTIONS } = config;
76
+ return new Intl.DateTimeFormat(locale, options).format(new Date(date));
77
+ };
78
+ //#endregion
79
+ //#region src/collection-query.ts
80
+ const toInt = (value, fallback) => {
81
+ const parsed = Number.parseInt(value ?? "", 10);
82
+ return Number.isFinite(parsed) ? parsed : fallback;
83
+ };
84
+ const parseCollectionQuery = (searchParams, options = {}) => {
85
+ const { defaultPage = 1, defaultSize = 12, maxSize = 100 } = options;
86
+ const page = Math.max(1, toInt(searchParams.get("page"), defaultPage));
87
+ const size = Math.min(maxSize, Math.max(1, toInt(searchParams.get("size"), defaultSize)));
88
+ const fallbackOffset = (page - 1) * size;
89
+ const offset = Math.max(0, toInt(searchParams.get("offset"), fallbackOffset));
90
+ const rawFilters = searchParams.get("filters");
91
+ let filters = {};
92
+ if (rawFilters) try {
93
+ const parsed = JSON.parse(rawFilters);
94
+ filters = Object.fromEntries(Object.entries(parsed).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, String(value)]));
95
+ } catch {
96
+ filters = {};
97
+ }
98
+ return {
99
+ page,
100
+ size,
101
+ offset,
102
+ filters
103
+ };
104
+ };
105
+ const paginateCollection = (list, query) => {
106
+ const totalItems = list.length;
107
+ const totalPages = Math.max(1, Math.ceil(totalItems / query.size));
108
+ const start = (Math.min(totalPages, Math.max(1, Math.floor(query.offset / query.size) + 1)) - 1) * query.size;
109
+ const end = start + query.size;
110
+ return {
111
+ items: list.slice(start, end),
112
+ totalItems,
113
+ totalPages,
114
+ start,
115
+ end: Math.min(end, totalItems)
116
+ };
117
+ };
118
+ const buildCollectionHref = (pathname, query, overrides = {}) => {
119
+ const next = {
120
+ ...query,
121
+ ...overrides
122
+ };
123
+ const normalizedOffset = (next.page - 1) * next.size;
124
+ const params = new URLSearchParams({
125
+ page: String(next.page),
126
+ size: String(next.size),
127
+ offset: String(next.offset ?? normalizedOffset)
128
+ });
129
+ if (Object.keys(next.filters ?? {}).length > 0) params.set("filters", JSON.stringify(next.filters));
130
+ return `${pathname}?${params.toString()}`;
131
+ };
132
+ const matchesCollectionFilters = (values, filters) => Object.entries(filters).every(([key, value]) => {
133
+ return (values[key] ?? []).includes(value);
40
134
  });
41
- var import_resend = require("resend");
42
- async function sendEmail(config, payload) {
43
- if (config.mock) {
44
- console.info("[astroflare:forms] Mock email payload", payload);
45
- return { id: "mock-email-id" };
46
- }
47
- const resend = new import_resend.Resend(config.apiKey);
48
- const { data, error } = await resend.emails.send({
49
- from: payload.from,
50
- to: payload.to,
51
- subject: payload.subject,
52
- html: payload.html
53
- });
54
- if (error) {
55
- console.error("[astroflare:forms] Resend API error", error);
56
- throw new Error(`Failed to send email: ${error.message ?? "Unknown error"}`);
57
- }
58
- return data;
59
- }
60
- function composeEmailAddress(local, domain, tag) {
61
- const taggedLocal = tag ? `${local}+${tag}` : local;
62
- return `${taggedLocal}@${domain}`;
63
- }
64
- function generateFormSectionHtml(section) {
65
- const itemsHtml = section.items.map((item) => {
66
- const value = item.value ?? "";
67
- const isHtml = /<[^>]+>/.test(value);
68
- return `
69
- <div style="margin-bottom:16px;">
70
- <p style="margin:0 0 4px;font-weight:600;color:#111827;">${item.key}</p>
71
- ${isHtml ? `<div style="margin:0;color:#111827;line-height:1.6;">${value || "Not specified"}</div>` : `<p style="margin:0;color:#111827;line-height:1.6;white-space:pre-wrap;">${value || "Not specified"}</p>`}
72
- </div>
73
- `;
74
- }).join("");
75
- return `
76
- <section style="background:#f3f4f6;padding:16px;border-radius:8px;margin:16px 0;border:1px solid #e5e7eb;">
77
- <h3 style="margin:0 0 12px;color:#111827;font-size:16px;font-weight:600;">${section.title}</h3>
78
- <div style="color:#111827;">
79
- ${itemsHtml}
80
- </div>
81
- </section>
82
- `;
83
- }
84
- function generateFormResultHtml(sections) {
85
- return sections.map(generateFormSectionHtml).join("\n");
86
- }
87
- async function renderEmailTemplate(args) {
88
- const { title, contentHtml, brandName, footerText } = args;
89
- const footer = footerText ?? `This message was generated by the ${brandName ?? "site"} contact form on ${(/* @__PURE__ */ new Date()).toLocaleString(
90
- "en-GB"
91
- )}.`;
92
- return `<!DOCTYPE html>
93
- <html lang="en">
94
- <head>
95
- <meta charset="utf-8">
96
- <meta name="viewport" content="width=device-width, initial-scale=1">
97
- <title>${title}</title>
98
- <meta http-equiv="x-ua-compatible" content="ie=edge">
99
- </head>
100
- <body style="margin:0;padding:0;background:#f3f4f6;color:#111827;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';">
101
- <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#f3f4f6;">
102
- <tr>
103
- <td align="center" style="padding:24px;">
104
- <table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="max-width:720px;background:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #e5e7eb;">
105
- <tr>
106
- <td style="padding:24px 24px 16px 24px;border-bottom:1px solid #e5e7eb;">
107
- <h1 style="margin:0 0 8px 0;font-size:20px;line-height:28px;color:#111827;letter-spacing:0.3px;">
108
- ${title}
109
- </h1>
110
- <p style="margin:0;font-size:13px;color:#6b7280;line-height:20px;">
111
- Contact form submission
112
- </p>
113
- </td>
114
- </tr>
115
- <tr>
116
- <td style="padding:24px;">
117
- ${contentHtml}
118
- </td>
119
- </tr>
120
- <tr>
121
- <td style="padding:16px 24px;background:#f9fafb;border-top:1px solid #e5e7eb;">
122
- <p style="margin:0;color:#6b7280;font-size:12px;line-height:18px;">
123
- ${footer}
124
- </p>
125
- </td>
126
- </tr>
127
- </table>
128
- </td>
129
- </tr>
130
- </table>
131
- </body>
132
- </html>`;
133
- }
134
- // Annotate the CommonJS export names for ESM import in node:
135
- 0 && (module.exports = {
136
- composeEmailAddress,
137
- forms,
138
- generateFormResultHtml,
139
- generateFormSectionHtml,
140
- renderEmailTemplate,
141
- sendEmail
135
+ const buildPageSequence = (totalPages, currentPage, maxButtons = 7) => {
136
+ if (totalPages <= maxButtons) return Array.from({ length: totalPages }, (_, index) => index + 1);
137
+ const innerSlots = Math.max(1, maxButtons - 2);
138
+ const left = Math.max(2, currentPage - Math.floor(innerSlots / 2));
139
+ const right = Math.min(totalPages - 1, left + innerSlots - 1);
140
+ const adjustedLeft = Math.max(2, right - innerSlots + 1);
141
+ const sequence = [1];
142
+ if (adjustedLeft > 2) sequence.push("…");
143
+ for (let page = adjustedLeft; page <= right; page += 1) sequence.push(page);
144
+ if (right < totalPages - 1) sequence.push("…");
145
+ sequence.push(totalPages);
146
+ return sequence;
147
+ };
148
+ //#endregion
149
+ exports.buildCollectionHref = buildCollectionHref;
150
+ exports.buildPageSequence = buildPageSequence;
151
+ exports.composeEmailAddress = require_forms.composeEmailAddress;
152
+ exports.formatDisplayDate = formatDisplayDate;
153
+ Object.defineProperty(exports, "forms", {
154
+ enumerable: true,
155
+ get: function() {
156
+ return require_forms.forms_exports;
157
+ }
142
158
  });
159
+ exports.generateFormResultHtml = require_forms.generateFormResultHtml;
160
+ exports.generateFormSectionHtml = require_forms.generateFormSectionHtml;
161
+ exports.getTagPalette = getTagPalette;
162
+ exports.matchesCollectionFilters = matchesCollectionFilters;
163
+ exports.paginateCollection = paginateCollection;
164
+ exports.parseCollectionQuery = parseCollectionQuery;
165
+ exports.renderEmailTemplate = require_forms.renderEmailTemplate;
166
+ exports.sendEmail = require_forms.sendEmail;
package/dist/core.d.cts CHANGED
@@ -1,2 +1,59 @@
1
- export { EmailPayload, FormSection, ResendConfig, composeEmailAddress, i as forms, generateFormResultHtml, generateFormSectionHtml, renderEmailTemplate, sendEmail } from './forms/index.cjs';
2
- import 'resend';
1
+ import { a as generateFormResultHtml, c as renderEmailTemplate, i as composeEmailAddress, l as sendEmail, n as FormSection, o as generateFormSectionHtml, r as ResendConfig, s as index_d_exports, t as EmailPayload } from "./index-CcH_yXr-.cjs";
2
+
3
+ //#region src/tag-colors.d.ts
4
+ interface TagPalette {
5
+ /** Soft background fill for the tag surface. */
6
+ bg: string;
7
+ /** Border color paired with `bg` for tag outlines. */
8
+ border: string;
9
+ /** Foreground text color with readable contrast on `bg`. */
10
+ text: string;
11
+ }
12
+ interface TagColorOptions {
13
+ /** Optional explicit tag-key to hex color map (expects lowercase keys). */
14
+ overrides?: Record<string, string>;
15
+ }
16
+ /**
17
+ * Build a deterministic HSL-based palette for a tag label.
18
+ * Uses known defaults and optional overrides before falling back to hashed hue.
19
+ */
20
+ declare const getTagPalette: (tag: string, options?: TagColorOptions) => TagPalette;
21
+ //#endregion
22
+ //#region src/date-format.d.ts
23
+ interface DateFormatOptions {
24
+ /** BCP 47 locale tag used by `Intl.DateTimeFormat`. */
25
+ locale?: string;
26
+ /** Intl formatting options merged with defaults. */
27
+ options?: Intl.DateTimeFormatOptions;
28
+ }
29
+ /**
30
+ * Format dates consistently for cards and article metadata.
31
+ */
32
+ declare const formatDisplayDate: (date: Date | string | number, config?: DateFormatOptions) => string;
33
+ //#endregion
34
+ //#region src/collection-query.d.ts
35
+ interface CollectionQueryOptions {
36
+ defaultPage?: number;
37
+ defaultSize?: number;
38
+ maxSize?: number;
39
+ }
40
+ interface CollectionQueryState {
41
+ page: number;
42
+ size: number;
43
+ offset: number;
44
+ filters: Record<string, string>;
45
+ }
46
+ interface PaginationSlice<T> {
47
+ items: T[];
48
+ totalItems: number;
49
+ totalPages: number;
50
+ start: number;
51
+ end: number;
52
+ }
53
+ declare const parseCollectionQuery: (searchParams: URLSearchParams, options?: CollectionQueryOptions) => CollectionQueryState;
54
+ declare const paginateCollection: <T>(list: T[], query: CollectionQueryState) => PaginationSlice<T>;
55
+ declare const buildCollectionHref: (pathname: string, query: CollectionQueryState, overrides?: Partial<CollectionQueryState>) => string;
56
+ declare const matchesCollectionFilters: (values: Record<string, string[]>, filters: Record<string, string>) => boolean;
57
+ declare const buildPageSequence: (totalPages: number, currentPage: number, maxButtons?: number) => Array<number | "\u2026">;
58
+ //#endregion
59
+ export { CollectionQueryOptions, CollectionQueryState, DateFormatOptions, EmailPayload, FormSection, PaginationSlice, ResendConfig, TagColorOptions, TagPalette, buildCollectionHref, buildPageSequence, composeEmailAddress, formatDisplayDate, index_d_exports as forms, generateFormResultHtml, generateFormSectionHtml, getTagPalette, matchesCollectionFilters, paginateCollection, parseCollectionQuery, renderEmailTemplate, sendEmail };
package/dist/core.d.ts CHANGED
@@ -1,2 +1,59 @@
1
- export { EmailPayload, FormSection, ResendConfig, composeEmailAddress, i as forms, generateFormResultHtml, generateFormSectionHtml, renderEmailTemplate, sendEmail } from './forms/index.js';
2
- import 'resend';
1
+ import { a as generateFormResultHtml, c as renderEmailTemplate, i as composeEmailAddress, l as sendEmail, n as FormSection, o as generateFormSectionHtml, r as ResendConfig, s as index_d_exports, t as EmailPayload } from "./index-Bk-K9aDQ.js";
2
+
3
+ //#region src/tag-colors.d.ts
4
+ interface TagPalette {
5
+ /** Soft background fill for the tag surface. */
6
+ bg: string;
7
+ /** Border color paired with `bg` for tag outlines. */
8
+ border: string;
9
+ /** Foreground text color with readable contrast on `bg`. */
10
+ text: string;
11
+ }
12
+ interface TagColorOptions {
13
+ /** Optional explicit tag-key to hex color map (expects lowercase keys). */
14
+ overrides?: Record<string, string>;
15
+ }
16
+ /**
17
+ * Build a deterministic HSL-based palette for a tag label.
18
+ * Uses known defaults and optional overrides before falling back to hashed hue.
19
+ */
20
+ declare const getTagPalette: (tag: string, options?: TagColorOptions) => TagPalette;
21
+ //#endregion
22
+ //#region src/date-format.d.ts
23
+ interface DateFormatOptions {
24
+ /** BCP 47 locale tag used by `Intl.DateTimeFormat`. */
25
+ locale?: string;
26
+ /** Intl formatting options merged with defaults. */
27
+ options?: Intl.DateTimeFormatOptions;
28
+ }
29
+ /**
30
+ * Format dates consistently for cards and article metadata.
31
+ */
32
+ declare const formatDisplayDate: (date: Date | string | number, config?: DateFormatOptions) => string;
33
+ //#endregion
34
+ //#region src/collection-query.d.ts
35
+ interface CollectionQueryOptions {
36
+ defaultPage?: number;
37
+ defaultSize?: number;
38
+ maxSize?: number;
39
+ }
40
+ interface CollectionQueryState {
41
+ page: number;
42
+ size: number;
43
+ offset: number;
44
+ filters: Record<string, string>;
45
+ }
46
+ interface PaginationSlice<T> {
47
+ items: T[];
48
+ totalItems: number;
49
+ totalPages: number;
50
+ start: number;
51
+ end: number;
52
+ }
53
+ declare const parseCollectionQuery: (searchParams: URLSearchParams, options?: CollectionQueryOptions) => CollectionQueryState;
54
+ declare const paginateCollection: <T>(list: T[], query: CollectionQueryState) => PaginationSlice<T>;
55
+ declare const buildCollectionHref: (pathname: string, query: CollectionQueryState, overrides?: Partial<CollectionQueryState>) => string;
56
+ declare const matchesCollectionFilters: (values: Record<string, string[]>, filters: Record<string, string>) => boolean;
57
+ declare const buildPageSequence: (totalPages: number, currentPage: number, maxButtons?: number) => Array<number | "\u2026">;
58
+ //#endregion
59
+ export { CollectionQueryOptions, CollectionQueryState, DateFormatOptions, EmailPayload, FormSection, PaginationSlice, ResendConfig, TagColorOptions, TagPalette, buildCollectionHref, buildPageSequence, composeEmailAddress, formatDisplayDate, index_d_exports as forms, generateFormResultHtml, generateFormSectionHtml, getTagPalette, matchesCollectionFilters, paginateCollection, parseCollectionQuery, renderEmailTemplate, sendEmail };