@at-flux/astroflare 1.0.2 → 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 (41) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +98 -14
  3. package/dist/chunk-pbuEa-1d.js +13 -0
  4. package/dist/core.cjs +162 -138
  5. package/dist/core.d.cts +59 -2
  6. package/dist/core.d.ts +59 -2
  7. package/dist/core.js +147 -16
  8. package/dist/forms/index.cjs +7 -130
  9. package/dist/forms/index.d.cts +2 -52
  10. package/dist/forms/index.d.ts +2 -52
  11. package/dist/forms/index.js +99 -14
  12. package/dist/{chunk-DRYHJSYC.js → forms-8uIdWmMD.cjs} +81 -53
  13. package/dist/index-Bk-K9aDQ.d.ts +44 -0
  14. package/dist/index-CcH_yXr-.d.cts +44 -0
  15. package/dist/index.cjs +20 -141
  16. package/dist/index.d.cts +3 -2
  17. package/dist/index.d.ts +3 -2
  18. package/dist/index.js +3 -17
  19. package/package.json +14 -8
  20. package/src/components/CollectionQuery.astro +107 -0
  21. package/src/components/ContactModalCta.astro +2 -0
  22. package/src/components/FilterPills.astro +164 -0
  23. package/src/components/IconButton.astro +5 -0
  24. package/src/components/InstagramProfileLink.astro +3 -0
  25. package/src/components/ListSummary.astro +63 -0
  26. package/src/components/MediaProtect.astro +41 -0
  27. package/src/components/Modal.astro +3 -0
  28. package/src/components/ModalTrigger.astro +2 -0
  29. package/src/components/Pager.astro +92 -0
  30. package/src/components/Section.astro +3 -0
  31. package/src/components/TagSummary.astro +71 -0
  32. package/src/components/ThemeToggle.astro +1 -0
  33. package/src/components/Tooltip.astro +70 -0
  34. package/src/components/collection-query-props.ts +45 -0
  35. package/src/runtime/collection-query.ts +156 -0
  36. package/src/runtime/media-protect.ts +70 -0
  37. package/src/styles/accessibility.css +1 -1
  38. package/src/styles/no-save.css +2 -2
  39. package/src/styles/prose.css +23 -11
  40. package/src/styles/scrollbar.css +10 -3
  41. package/dist/chunk-ALMPH3A2.js +0 -0
@@ -0,0 +1,156 @@
1
+ export interface CollectionQueryRuntimeOptions {
2
+ cardSelector: string;
3
+ filterSelector: string;
4
+ paginationSelector: string;
5
+ perPage: number;
6
+ activeClass: string;
7
+ tagSeparator: string;
8
+ pageButtonClass: string;
9
+ ellipsisClass: string;
10
+ maxPageButtons: number;
11
+ }
12
+
13
+ const DEFAULTS: CollectionQueryRuntimeOptions = {
14
+ cardSelector: "[data-card]",
15
+ filterSelector: "[data-filter]",
16
+ paginationSelector: "[data-pagination]",
17
+ perPage: 9,
18
+ activeClass: "is-active",
19
+ tagSeparator: "|",
20
+ pageButtonClass: "af-pager-pill",
21
+ ellipsisClass: "af-pager-ellipsis",
22
+ maxPageButtons: 7,
23
+ };
24
+
25
+ const getConfig = (root: HTMLElement): CollectionQueryRuntimeOptions => {
26
+ const {
27
+ cardSelector,
28
+ filterSelector,
29
+ paginationSelector,
30
+ perPage,
31
+ activeClass,
32
+ tagSeparator,
33
+ pageButtonClass,
34
+ ellipsisClass,
35
+ maxPageButtons,
36
+ } = root.dataset;
37
+
38
+ return {
39
+ cardSelector: cardSelector ?? DEFAULTS.cardSelector,
40
+ filterSelector: filterSelector ?? DEFAULTS.filterSelector,
41
+ paginationSelector: paginationSelector ?? DEFAULTS.paginationSelector,
42
+ perPage: Number(perPage ?? DEFAULTS.perPage),
43
+ activeClass: activeClass ?? DEFAULTS.activeClass,
44
+ tagSeparator: tagSeparator ?? DEFAULTS.tagSeparator,
45
+ pageButtonClass: pageButtonClass ?? DEFAULTS.pageButtonClass,
46
+ ellipsisClass: ellipsisClass ?? DEFAULTS.ellipsisClass,
47
+ maxPageButtons: Number(maxPageButtons ?? DEFAULTS.maxPageButtons),
48
+ };
49
+ };
50
+
51
+ export const getClientPageSequence = (
52
+ totalPages: number,
53
+ currentPage: number,
54
+ maxButtons: number,
55
+ ): Array<number | "…"> => {
56
+ if (totalPages <= maxButtons) {
57
+ return Array.from({ length: totalPages }, (_, index) => index + 1);
58
+ }
59
+
60
+ const innerSlots = Math.max(1, maxButtons - 2);
61
+ const left = Math.max(2, currentPage - Math.floor(innerSlots / 2));
62
+ const right = Math.min(totalPages - 1, left + innerSlots - 1);
63
+ const adjustedLeft = Math.max(2, right - innerSlots + 1);
64
+
65
+ const sequence: Array<number | "…"> = [1];
66
+ if (adjustedLeft > 2) sequence.push("…");
67
+ for (let page = adjustedLeft; page <= right; page += 1) sequence.push(page);
68
+ if (right < totalPages - 1) sequence.push("…");
69
+ sequence.push(totalPages);
70
+ return sequence;
71
+ };
72
+
73
+ export const initCollectionQueryElement = (root: HTMLElement): void => {
74
+ if (root.dataset.afInit === "true") return;
75
+ root.dataset.afInit = "true";
76
+
77
+ const config = getConfig(root);
78
+ const cards = Array.from(root.querySelectorAll<HTMLElement>(config.cardSelector));
79
+ const filters = Array.from(root.querySelectorAll<HTMLButtonElement>(config.filterSelector));
80
+ const pagination = root.querySelector<HTMLElement>(config.paginationSelector);
81
+
82
+ let page = 1;
83
+ let activeFilter = filters.find((button) => button.classList.contains(config.activeClass))?.dataset.filter ?? "all";
84
+
85
+ const filterCards = (): HTMLElement[] =>
86
+ cards.filter((card) => {
87
+ if (activeFilter === "all") return true;
88
+ const tags = (card.dataset.tags ?? "").split(config.tagSeparator);
89
+ return tags.includes(activeFilter);
90
+ });
91
+
92
+ const renderPagination = (totalPages: number) => {
93
+ if (!pagination) return;
94
+ pagination.innerHTML = "";
95
+ if (totalPages <= 1) return;
96
+
97
+ const pages = getClientPageSequence(totalPages, page, config.maxPageButtons);
98
+ for (const entry of pages) {
99
+ if (entry === "…") {
100
+ const ellipsis = document.createElement("span");
101
+ ellipsis.className = config.ellipsisClass;
102
+ ellipsis.textContent = "…";
103
+ ellipsis.setAttribute("aria-hidden", "true");
104
+ pagination.appendChild(ellipsis);
105
+ continue;
106
+ }
107
+
108
+ const button = document.createElement("button");
109
+ button.type = "button";
110
+ button.dataset.page = String(entry);
111
+ button.textContent = String(entry);
112
+ button.className = [config.pageButtonClass, entry === page ? config.activeClass : ""]
113
+ .filter(Boolean)
114
+ .join(" ");
115
+ button.addEventListener("click", () => {
116
+ page = entry;
117
+ render();
118
+ });
119
+ pagination.appendChild(button);
120
+ }
121
+ };
122
+
123
+ const render = () => {
124
+ const filtered = filterCards();
125
+ const totalPages = Math.max(1, Math.ceil(filtered.length / config.perPage));
126
+ page = Math.min(page, totalPages);
127
+
128
+ const start = (page - 1) * config.perPage;
129
+ const end = start + config.perPage;
130
+
131
+ cards.forEach((card) => {
132
+ card.style.display = "none";
133
+ });
134
+ filtered.slice(start, end).forEach((card) => {
135
+ card.style.display = "";
136
+ });
137
+
138
+ renderPagination(totalPages);
139
+ };
140
+
141
+ filters.forEach((button) => {
142
+ button.addEventListener("click", () => {
143
+ activeFilter = button.dataset.filter ?? "all";
144
+ page = 1;
145
+ filters.forEach((item) => item.classList.toggle(config.activeClass, item === button));
146
+ render();
147
+ });
148
+ });
149
+
150
+ render();
151
+ };
152
+
153
+ export const initCollectionQueryRoots = (scope: ParentNode = document): void => {
154
+ const roots = Array.from(scope.querySelectorAll<HTMLElement>("collection-query[data-af-query]"));
155
+ roots.forEach((root) => initCollectionQueryElement(root));
156
+ };
@@ -0,0 +1,70 @@
1
+ interface MediaProtectState {
2
+ drag: boolean;
3
+ contextMenu: boolean;
4
+ }
5
+
6
+ declare global {
7
+ interface Window {
8
+ __astroflareMediaProtectHandlerState__?: MediaProtectState;
9
+ }
10
+ }
11
+
12
+ const STATE_KEY = "__astroflareMediaProtectHandlerState__";
13
+
14
+ const shouldPrevent = (target: EventTarget | null, mode: "drag" | "contextMenu"): boolean => {
15
+ if (!(target instanceof Element)) return false;
16
+ const root = target.closest<HTMLElement>("[data-media-protect-root]");
17
+ if (!root) return false;
18
+ return mode === "drag"
19
+ ? root.dataset.mediaProtectDrag === "true"
20
+ : root.dataset.mediaProtectContextMenu === "true";
21
+ };
22
+
23
+ const ensureHandlers = (options: { drag: boolean; contextMenu: boolean }) => {
24
+ const runtime = window;
25
+ const state = runtime[STATE_KEY] ?? { drag: false, contextMenu: false };
26
+ runtime[STATE_KEY] = state;
27
+
28
+ if (options.contextMenu && !state.contextMenu) {
29
+ document.addEventListener(
30
+ "contextmenu",
31
+ (event) => {
32
+ if (shouldPrevent(event.target, "contextMenu")) event.preventDefault();
33
+ },
34
+ { capture: true },
35
+ );
36
+ state.contextMenu = true;
37
+ }
38
+
39
+ if (options.drag && !state.drag) {
40
+ document.addEventListener(
41
+ "dragstart",
42
+ (event) => {
43
+ if (shouldPrevent(event.target, "drag")) event.preventDefault();
44
+ },
45
+ { capture: true },
46
+ );
47
+ state.drag = true;
48
+ }
49
+ };
50
+
51
+ export const initMediaProtectRoots = (scope: ParentNode = document): void => {
52
+ const roots = Array.from(
53
+ scope.querySelectorAll<HTMLElement>(
54
+ "[data-media-protect-root]:not([data-media-protect-init='true'])",
55
+ ),
56
+ );
57
+ if (roots.length === 0) return;
58
+
59
+ let dragNeeded = false;
60
+ let contextMenuNeeded = false;
61
+
62
+ roots.forEach((root) => {
63
+ root.dataset.mediaProtectInit = "true";
64
+ dragNeeded = dragNeeded || root.dataset.mediaProtectDrag === "true";
65
+ contextMenuNeeded =
66
+ contextMenuNeeded || root.dataset.mediaProtectContextMenu === "true";
67
+ });
68
+
69
+ ensureHandlers({ drag: dragNeeded, contextMenu: contextMenuNeeded });
70
+ };
@@ -13,7 +13,7 @@
13
13
  background: color-mix(in oklab, var(--color-brand, #7c5cff) 25%, transparent);
14
14
  }
15
15
 
16
- :where([data-theme='dark']) ::selection {
16
+ :where([data-theme="dark"]) ::selection {
17
17
  background: color-mix(in oklab, var(--color-brand, #7c5cff) 35%, transparent);
18
18
  }
19
19
 
@@ -20,14 +20,14 @@
20
20
  }
21
21
 
22
22
  .no-save-container::before {
23
- content: '';
23
+ content: "";
24
24
  position: absolute;
25
25
  inset: 0;
26
26
  z-index: 1;
27
27
  }
28
28
 
29
29
  a:has(.no-save)::after {
30
- content: '';
30
+ content: "";
31
31
  position: absolute;
32
32
  inset: 0;
33
33
  z-index: 2;
@@ -3,7 +3,9 @@
3
3
  * Uses CSS custom property --color-brand for theming.
4
4
  * Apply .prose to the container element.
5
5
  */
6
- .prose { color: inherit; }
6
+ .prose {
7
+ color: inherit;
8
+ }
7
9
 
8
10
  .prose h1,
9
11
  .prose h2,
@@ -20,8 +22,12 @@
20
22
  line-height: 1.6;
21
23
  }
22
24
 
23
- .prose a { color: var(--color-brand, #7c5cff); }
24
- .prose a:hover { opacity: 0.8; }
25
+ .prose a {
26
+ color: var(--color-brand, #7c5cff);
27
+ }
28
+ .prose a:hover {
29
+ opacity: 0.8;
30
+ }
25
31
 
26
32
  .prose code {
27
33
  background: rgba(0, 0, 0, 0.05);
@@ -30,7 +36,7 @@
30
36
  font-size: 0.9em;
31
37
  }
32
38
 
33
- :where([data-theme='dark']) .prose code {
39
+ :where([data-theme="dark"]) .prose code {
34
40
  background: rgba(255, 255, 255, 0.1);
35
41
  }
36
42
 
@@ -52,7 +58,9 @@
52
58
  padding-left: 2em;
53
59
  }
54
60
 
55
- .prose li { margin: 0.5em 0; }
61
+ .prose li {
62
+ margin: 0.5em 0;
63
+ }
56
64
 
57
65
  .prose blockquote {
58
66
  margin: 2em 0;
@@ -63,12 +71,16 @@
63
71
  font-style: italic;
64
72
  }
65
73
 
66
- :where([data-theme='dark']) .prose blockquote {
74
+ :where([data-theme="dark"]) .prose blockquote {
67
75
  background: color-mix(in oklab, var(--color-brand, #7c5cff) 12%, transparent);
68
76
  }
69
77
 
70
- .prose blockquote p { margin-bottom: 0.5rem; }
71
- .prose blockquote p:last-child { margin-bottom: 0; }
78
+ .prose blockquote p {
79
+ margin-bottom: 0.5rem;
80
+ }
81
+ .prose blockquote p:last-child {
82
+ margin-bottom: 0;
83
+ }
72
84
 
73
85
  .prose img {
74
86
  max-width: 100%;
@@ -90,8 +102,8 @@
90
102
  text-align: left;
91
103
  }
92
104
 
93
- :where([data-theme='dark']) .prose th,
94
- :where([data-theme='dark']) .prose td {
105
+ :where([data-theme="dark"]) .prose th,
106
+ :where([data-theme="dark"]) .prose td {
95
107
  border-color: rgba(255, 255, 255, 0.1);
96
108
  }
97
109
 
@@ -100,6 +112,6 @@
100
112
  background: rgba(0, 0, 0, 0.05);
101
113
  }
102
114
 
103
- :where([data-theme='dark']) .prose th {
115
+ :where([data-theme="dark"]) .prose th {
104
116
  background: rgba(255, 255, 255, 0.05);
105
117
  }
@@ -2,10 +2,17 @@
2
2
  * Branded scrollbar styling.
3
3
  * Uses --color-brand and --color-brand-pastel for theming.
4
4
  */
5
- ::-webkit-scrollbar { width: 8px; }
6
- ::-webkit-scrollbar-track { background: transparent; }
5
+ ::-webkit-scrollbar {
6
+ width: 8px;
7
+ }
8
+ ::-webkit-scrollbar-track {
9
+ background: transparent;
10
+ }
7
11
  ::-webkit-scrollbar-thumb {
8
- background: var(--color-brand-pastel, color-mix(in oklab, var(--color-brand, #7c5cff) 60%, white));
12
+ background: var(
13
+ --color-brand-pastel,
14
+ color-mix(in oklab, var(--color-brand, #7c5cff) 60%, white)
15
+ );
9
16
  border-radius: 4px;
10
17
  }
11
18
  ::-webkit-scrollbar-thumb:hover {
File without changes