@at-flux/astroflare 1.0.8 → 1.0.9
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/dist/core.cjs +17 -10
- package/dist/core.js +17 -10
- package/package.json +1 -1
- package/src/collection-query.ts +11 -2
- package/src/components/CollectionFooterControls.astro +60 -24
- package/src/components/CollectionQuery.astro +64 -10
- package/src/components/DateRangeFilterControls.astro +106 -0
- package/src/components/DisclosurePanel.astro +118 -0
- package/src/components/FilterPanel.astro +29 -0
- package/src/components/FilterPills.astro +44 -36
- package/src/components/FilterRow.astro +57 -0
- package/src/components/Pager.astro +1 -1
- package/src/components/TagSummary.astro +17 -4
- package/src/components/collection-query-props.ts +10 -0
- package/src/tag-colors.ts +8 -8
package/dist/core.cjs
CHANGED
|
@@ -2,11 +2,11 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
const require_forms = require("./forms-8uIdWmMD.cjs");
|
|
3
3
|
//#region src/tag-colors.ts
|
|
4
4
|
const DEFAULT_OVERRIDES = {
|
|
5
|
-
shoots: "#
|
|
6
|
-
tech: "#
|
|
7
|
-
ai: "#
|
|
8
|
-
sites: "#
|
|
9
|
-
flow: "#
|
|
5
|
+
shoots: "#c4786a",
|
|
6
|
+
tech: "#4d8c7a",
|
|
7
|
+
ai: "#d9773a",
|
|
8
|
+
sites: "#b8942e",
|
|
9
|
+
flow: "#9b6bc9"
|
|
10
10
|
};
|
|
11
11
|
const hashStringToHue = (input) => {
|
|
12
12
|
let hash = 0;
|
|
@@ -56,9 +56,9 @@ const resolveHue = (tag, options) => {
|
|
|
56
56
|
const getTagPalette = (tag, options = {}) => {
|
|
57
57
|
const hue = resolveHue(tag, options);
|
|
58
58
|
return {
|
|
59
|
-
bg: `hsla(${hue}
|
|
60
|
-
border: `hsla(${hue}
|
|
61
|
-
text: `hsl(${hue}
|
|
59
|
+
bg: `hsla(${hue} 58% 56% / 0.16)`,
|
|
60
|
+
border: `hsla(${hue} 56% 46% / 0.42)`,
|
|
61
|
+
text: `hsl(${hue} 58% 32%)`
|
|
62
62
|
};
|
|
63
63
|
};
|
|
64
64
|
//#endregion
|
|
@@ -79,6 +79,13 @@ const formatDisplayDate = (date, config = {}) => {
|
|
|
79
79
|
//#region src/collection-query.ts
|
|
80
80
|
const normalizeFilterToken = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
|
|
81
81
|
const parseFilterValueList = (value) => (value ?? "").split(",").map(normalizeFilterToken).filter(Boolean).filter((token, index, list) => list.indexOf(token) === index);
|
|
82
|
+
const normalizeFilterValue = (key, value) => {
|
|
83
|
+
if (key === "date_from" || key === "date_to") {
|
|
84
|
+
const canonical = value.trim().replace(/_/g, "-");
|
|
85
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(canonical) ? canonical : "";
|
|
86
|
+
}
|
|
87
|
+
return parseFilterValueList(value).join(",");
|
|
88
|
+
};
|
|
82
89
|
const formatFilterValueListLabel = (value) => parseFilterValueList(value).map((token) => token.replace(/_/g, " ").toUpperCase()).join(", ");
|
|
83
90
|
const toInt = (value, fallback) => {
|
|
84
91
|
const parsed = Number.parseInt(value ?? "", 10);
|
|
@@ -95,7 +102,7 @@ const parseCollectionQuery = (searchParams, options = {}) => {
|
|
|
95
102
|
let filters = {};
|
|
96
103
|
if (rawFilters) try {
|
|
97
104
|
const parsed = JSON.parse(rawFilters);
|
|
98
|
-
filters = Object.fromEntries(Object.entries(parsed).filter(([, value]) => typeof value === "string").map(([key, value]) => [key,
|
|
105
|
+
filters = Object.fromEntries(Object.entries(parsed).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, normalizeFilterValue(key, String(value))]).filter(([, value]) => value.length > 0));
|
|
99
106
|
} catch {
|
|
100
107
|
filters = {};
|
|
101
108
|
}
|
|
@@ -131,7 +138,7 @@ const buildCollectionHref = (pathname, query, overrides = {}) => {
|
|
|
131
138
|
offset: String(offsetFromPage)
|
|
132
139
|
});
|
|
133
140
|
if (Object.keys(next.filters ?? {}).length > 0) {
|
|
134
|
-
const normalizedFilters = Object.fromEntries(Object.entries(next.filters).map(([key, value]) => [key,
|
|
141
|
+
const normalizedFilters = Object.fromEntries(Object.entries(next.filters).map(([key, value]) => [key, normalizeFilterValue(key, value)]));
|
|
135
142
|
params.set("filters", JSON.stringify(normalizedFilters));
|
|
136
143
|
}
|
|
137
144
|
return `${pathname}?${params.toString()}`;
|
package/dist/core.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { composeEmailAddress, generateFormResultHtml, generateFormSectionHtml, renderEmailTemplate, sendEmail, t as forms_exports } from "./forms/index.js";
|
|
2
2
|
//#region src/tag-colors.ts
|
|
3
3
|
const DEFAULT_OVERRIDES = {
|
|
4
|
-
shoots: "#
|
|
5
|
-
tech: "#
|
|
6
|
-
ai: "#
|
|
7
|
-
sites: "#
|
|
8
|
-
flow: "#
|
|
4
|
+
shoots: "#c4786a",
|
|
5
|
+
tech: "#4d8c7a",
|
|
6
|
+
ai: "#d9773a",
|
|
7
|
+
sites: "#b8942e",
|
|
8
|
+
flow: "#9b6bc9"
|
|
9
9
|
};
|
|
10
10
|
const hashStringToHue = (input) => {
|
|
11
11
|
let hash = 0;
|
|
@@ -55,9 +55,9 @@ const resolveHue = (tag, options) => {
|
|
|
55
55
|
const getTagPalette = (tag, options = {}) => {
|
|
56
56
|
const hue = resolveHue(tag, options);
|
|
57
57
|
return {
|
|
58
|
-
bg: `hsla(${hue}
|
|
59
|
-
border: `hsla(${hue}
|
|
60
|
-
text: `hsl(${hue}
|
|
58
|
+
bg: `hsla(${hue} 58% 56% / 0.16)`,
|
|
59
|
+
border: `hsla(${hue} 56% 46% / 0.42)`,
|
|
60
|
+
text: `hsl(${hue} 58% 32%)`
|
|
61
61
|
};
|
|
62
62
|
};
|
|
63
63
|
//#endregion
|
|
@@ -78,6 +78,13 @@ const formatDisplayDate = (date, config = {}) => {
|
|
|
78
78
|
//#region src/collection-query.ts
|
|
79
79
|
const normalizeFilterToken = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
|
|
80
80
|
const parseFilterValueList = (value) => (value ?? "").split(",").map(normalizeFilterToken).filter(Boolean).filter((token, index, list) => list.indexOf(token) === index);
|
|
81
|
+
const normalizeFilterValue = (key, value) => {
|
|
82
|
+
if (key === "date_from" || key === "date_to") {
|
|
83
|
+
const canonical = value.trim().replace(/_/g, "-");
|
|
84
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(canonical) ? canonical : "";
|
|
85
|
+
}
|
|
86
|
+
return parseFilterValueList(value).join(",");
|
|
87
|
+
};
|
|
81
88
|
const formatFilterValueListLabel = (value) => parseFilterValueList(value).map((token) => token.replace(/_/g, " ").toUpperCase()).join(", ");
|
|
82
89
|
const toInt = (value, fallback) => {
|
|
83
90
|
const parsed = Number.parseInt(value ?? "", 10);
|
|
@@ -94,7 +101,7 @@ const parseCollectionQuery = (searchParams, options = {}) => {
|
|
|
94
101
|
let filters = {};
|
|
95
102
|
if (rawFilters) try {
|
|
96
103
|
const parsed = JSON.parse(rawFilters);
|
|
97
|
-
filters = Object.fromEntries(Object.entries(parsed).filter(([, value]) => typeof value === "string").map(([key, value]) => [key,
|
|
104
|
+
filters = Object.fromEntries(Object.entries(parsed).filter(([, value]) => typeof value === "string").map(([key, value]) => [key, normalizeFilterValue(key, String(value))]).filter(([, value]) => value.length > 0));
|
|
98
105
|
} catch {
|
|
99
106
|
filters = {};
|
|
100
107
|
}
|
|
@@ -130,7 +137,7 @@ const buildCollectionHref = (pathname, query, overrides = {}) => {
|
|
|
130
137
|
offset: String(offsetFromPage)
|
|
131
138
|
});
|
|
132
139
|
if (Object.keys(next.filters ?? {}).length > 0) {
|
|
133
|
-
const normalizedFilters = Object.fromEntries(Object.entries(next.filters).map(([key, value]) => [key,
|
|
140
|
+
const normalizedFilters = Object.fromEntries(Object.entries(next.filters).map(([key, value]) => [key, normalizeFilterValue(key, value)]));
|
|
134
141
|
params.set("filters", JSON.stringify(normalizedFilters));
|
|
135
142
|
}
|
|
136
143
|
return `${pathname}?${params.toString()}`;
|
package/package.json
CHANGED
package/src/collection-query.ts
CHANGED
|
@@ -34,6 +34,14 @@ export const parseFilterValueList = (value: string | undefined): string[] =>
|
|
|
34
34
|
.filter(Boolean)
|
|
35
35
|
.filter((token, index, list) => list.indexOf(token) === index);
|
|
36
36
|
|
|
37
|
+
const normalizeFilterValue = (key: string, value: string): string => {
|
|
38
|
+
if (key === "date_from" || key === "date_to") {
|
|
39
|
+
const canonical = value.trim().replace(/_/g, "-");
|
|
40
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(canonical) ? canonical : "";
|
|
41
|
+
}
|
|
42
|
+
return parseFilterValueList(value).join(",");
|
|
43
|
+
};
|
|
44
|
+
|
|
37
45
|
export const formatFilterValueListLabel = (value: string | undefined): string =>
|
|
38
46
|
parseFilterValueList(value)
|
|
39
47
|
.map((token) => token.replace(/_/g, " ").toUpperCase())
|
|
@@ -71,7 +79,8 @@ export const parseCollectionQuery = (
|
|
|
71
79
|
filters = Object.fromEntries(
|
|
72
80
|
Object.entries(parsed)
|
|
73
81
|
.filter(([, value]) => typeof value === "string")
|
|
74
|
-
.map(([key, value]) => [key,
|
|
82
|
+
.map(([key, value]) => [key, normalizeFilterValue(key, String(value))])
|
|
83
|
+
.filter(([, value]) => value.length > 0),
|
|
75
84
|
);
|
|
76
85
|
} catch {
|
|
77
86
|
filters = {};
|
|
@@ -123,7 +132,7 @@ export const buildCollectionHref = (
|
|
|
123
132
|
const normalizedFilters = Object.fromEntries(
|
|
124
133
|
Object.entries(next.filters).map(([key, value]) => [
|
|
125
134
|
key,
|
|
126
|
-
|
|
135
|
+
normalizeFilterValue(key, value),
|
|
127
136
|
]),
|
|
128
137
|
);
|
|
129
138
|
params.set("filters", JSON.stringify(normalizedFilters));
|
|
@@ -9,8 +9,10 @@ import { buildCollectionHref, buildPageSequence, type CollectionQueryState } fro
|
|
|
9
9
|
interface Props {
|
|
10
10
|
pathname: string;
|
|
11
11
|
query: CollectionQueryState;
|
|
12
|
+
stickyParams?: Record<string, string>;
|
|
12
13
|
totalPages: number;
|
|
13
14
|
currentPage: number;
|
|
15
|
+
totalItems?: number;
|
|
14
16
|
sizeOptions?: number[];
|
|
15
17
|
maxPageButtons?: number;
|
|
16
18
|
class?: string;
|
|
@@ -19,8 +21,10 @@ interface Props {
|
|
|
19
21
|
const {
|
|
20
22
|
pathname,
|
|
21
23
|
query,
|
|
24
|
+
stickyParams = {},
|
|
22
25
|
totalPages,
|
|
23
26
|
currentPage,
|
|
27
|
+
totalItems = 0,
|
|
24
28
|
sizeOptions = [4, 2, 8, 12, 16],
|
|
25
29
|
maxPageButtons = 7,
|
|
26
30
|
class: className = "",
|
|
@@ -40,34 +44,58 @@ const pagerItems = buildPageSequence(totalPages, currentPage, maxPageButtons).ma
|
|
|
40
44
|
const sanitizedSizeOptions = Array.from(
|
|
41
45
|
new Set([query.size, ...sizeOptions].filter((value) => Number.isFinite(value) && value > 0)),
|
|
42
46
|
).sort((a, b) => a - b);
|
|
47
|
+
const pageSizeDisabled = totalItems === 0;
|
|
48
|
+
const pageSizeDisabledTitle = "Unavailable ⛔ Nothing to show. Reset filters to change page size.";
|
|
49
|
+
const withStickyParams = (href: string): string => {
|
|
50
|
+
if (Object.keys(stickyParams).length === 0) return href;
|
|
51
|
+
const url = new URL(href, "https://astroflare.local");
|
|
52
|
+
for (const [key, value] of Object.entries(stickyParams)) {
|
|
53
|
+
url.searchParams.set(key, value);
|
|
54
|
+
}
|
|
55
|
+
return `${url.pathname}?${url.searchParams.toString()}`;
|
|
56
|
+
};
|
|
57
|
+
const pagerItemsWithSticky = pagerItems.map((item) =>
|
|
58
|
+
"href" in item && typeof item.href === "string"
|
|
59
|
+
? { ...item, href: withStickyParams(item.href) }
|
|
60
|
+
: item,
|
|
61
|
+
);
|
|
43
62
|
---
|
|
44
63
|
|
|
45
|
-
<div class:list={["af-collection-footer grid gap-3
|
|
46
|
-
<div class="af-collection-
|
|
64
|
+
<div class:list={["af-collection-footer grid gap-3", className]}>
|
|
65
|
+
<div class="af-collection-controls-row flex items-center justify-between gap-3">
|
|
66
|
+
<Pager items={pagerItemsWithSticky} />
|
|
67
|
+
<form method="get" action={pathname} data-astro-reload>
|
|
68
|
+
<input type="hidden" name="page" value="1" />
|
|
69
|
+
<input type="hidden" name="offset" value="0" />
|
|
70
|
+
{Object.entries(stickyParams).map(([key, value]) => (
|
|
71
|
+
<input type="hidden" name={key} value={value} />
|
|
72
|
+
))}
|
|
73
|
+
{Object.keys(activeFilters).length > 0 && (
|
|
74
|
+
<input type="hidden" name="filters" value={JSON.stringify(activeFilters)} />
|
|
75
|
+
)}
|
|
76
|
+
<label
|
|
77
|
+
class:list={["af-page-size-label inline-flex items-center gap-2 text-xs uppercase tracking-[0.08em] text-light-text-subtle dark:text-dark-text-subtle", pageSizeDisabled && "is-disabled"]}
|
|
78
|
+
title={pageSizeDisabled ? pageSizeDisabledTitle : undefined}
|
|
79
|
+
>
|
|
80
|
+
<span>Page size</span>
|
|
81
|
+
<select
|
|
82
|
+
name="size"
|
|
83
|
+
disabled={pageSizeDisabled}
|
|
84
|
+
class="select-none rounded-full border border-light-border bg-light-surface px-3 py-1.5 text-xs text-light-text-body dark:border-dark-border dark:bg-dark-surface dark:text-dark-text-body"
|
|
85
|
+
onchange="this.form?.submit()"
|
|
86
|
+
>
|
|
87
|
+
{sanitizedSizeOptions.map((size) => (
|
|
88
|
+
<option value={String(size)} selected={size === query.size}>
|
|
89
|
+
{size}
|
|
90
|
+
</option>
|
|
91
|
+
))}
|
|
92
|
+
</select>
|
|
93
|
+
</label>
|
|
94
|
+
</form>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="af-collection-summary text-right text-sm text-light-text-subtle dark:text-dark-text-subtle">
|
|
47
97
|
<slot name="summary" />
|
|
48
98
|
</div>
|
|
49
|
-
<Pager items={pagerItems} class="md:col-start-2 md:justify-center" />
|
|
50
|
-
<form method="get" action={pathname} data-astro-reload class="md:col-start-3 md:justify-self-end">
|
|
51
|
-
<input type="hidden" name="page" value="1" />
|
|
52
|
-
<input type="hidden" name="offset" value="0" />
|
|
53
|
-
{Object.keys(activeFilters).length > 0 && (
|
|
54
|
-
<input type="hidden" name="filters" value={JSON.stringify(activeFilters)} />
|
|
55
|
-
)}
|
|
56
|
-
<label class="af-page-size-label inline-flex items-center gap-2 text-xs uppercase tracking-[0.08em] text-light-text-subtle dark:text-dark-text-subtle">
|
|
57
|
-
<span>Page size</span>
|
|
58
|
-
<select
|
|
59
|
-
name="size"
|
|
60
|
-
class="select-none rounded-full border border-light-border bg-light-surface px-3 py-1.5 text-xs text-light-text-body dark:border-dark-border dark:bg-dark-surface dark:text-dark-text-body"
|
|
61
|
-
onchange="this.form?.submit()"
|
|
62
|
-
>
|
|
63
|
-
{sanitizedSizeOptions.map((size) => (
|
|
64
|
-
<option value={String(size)} selected={size === query.size}>
|
|
65
|
-
{size}
|
|
66
|
-
</option>
|
|
67
|
-
))}
|
|
68
|
-
</select>
|
|
69
|
-
</label>
|
|
70
|
-
</form>
|
|
71
99
|
</div>
|
|
72
100
|
|
|
73
101
|
<style is:global>
|
|
@@ -81,4 +109,12 @@ const sanitizedSizeOptions = Array.from(
|
|
|
81
109
|
user-select: none;
|
|
82
110
|
-webkit-user-select: none;
|
|
83
111
|
}
|
|
112
|
+
.af-page-size-label.is-disabled {
|
|
113
|
+
opacity: 0.58;
|
|
114
|
+
cursor: not-allowed;
|
|
115
|
+
}
|
|
116
|
+
.af-page-size-label.is-disabled select {
|
|
117
|
+
cursor: not-allowed;
|
|
118
|
+
border-style: dashed;
|
|
119
|
+
}
|
|
84
120
|
</style>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import FilterPills from "./FilterPills.astro";
|
|
3
|
+
import FilterPanel from "./FilterPanel.astro";
|
|
4
|
+
import FilterRow from "./FilterRow.astro";
|
|
3
5
|
import Pager from "./Pager.astro";
|
|
4
6
|
import {
|
|
5
7
|
buildCollectionHref,
|
|
@@ -13,10 +15,15 @@ type Props = CollectionQueryProps;
|
|
|
13
15
|
const {
|
|
14
16
|
useServer = false,
|
|
15
17
|
filters = [],
|
|
18
|
+
filtersTitle = "Filters",
|
|
19
|
+
filtersLabel = "Tags",
|
|
20
|
+
extraFiltersLabel = "Date",
|
|
21
|
+
showExtraFilters = false,
|
|
16
22
|
maxPageButtons = 7,
|
|
17
23
|
filtersClass = "mb-8",
|
|
18
24
|
pagerClass = "mt-6",
|
|
19
25
|
sizeOptions = [4, 2, 8, 12, 16],
|
|
26
|
+
stickyParams = {},
|
|
20
27
|
showPageSize = true,
|
|
21
28
|
pageSizeClass = "",
|
|
22
29
|
perPage = 9,
|
|
@@ -37,7 +44,10 @@ const normalize = (value: string): string => value.trim().toLowerCase();
|
|
|
37
44
|
const maxTagFilters = 5;
|
|
38
45
|
const activeTagTokens = parseFilterValueList(activeFilters.tag);
|
|
39
46
|
const lockFiltersToAll = totalItems === 0 && activeTagTokens.length >= 2;
|
|
40
|
-
const
|
|
47
|
+
const clearTagFilters = Object.fromEntries(
|
|
48
|
+
Object.entries(activeFilters).filter(([key]) => key !== "tag"),
|
|
49
|
+
);
|
|
50
|
+
const allHref = buildCollectionHref(pathname, query, { filters: clearTagFilters, page: 1, offset: 0 });
|
|
41
51
|
const serverFilterItems = filters.map((option) => {
|
|
42
52
|
const isTagFilter = option.key === "tag";
|
|
43
53
|
const normalizedOptionValue = normalizeFilterToken(option.value);
|
|
@@ -92,6 +102,26 @@ const pagerItems = buildPageSequence(totalPages, currentPage, maxPageButtons).ma
|
|
|
92
102
|
const sanitizedSizeOptions = Array.from(
|
|
93
103
|
new Set([query.size, ...sizeOptions].filter((value) => Number.isFinite(value) && value > 0)),
|
|
94
104
|
).sort((a, b) => a - b);
|
|
105
|
+
const pageSizeDisabled = totalItems === 0;
|
|
106
|
+
const pageSizeDisabledTitle = "Unavailable ⛔ Nothing to show. Reset filters to change page size.";
|
|
107
|
+
const withStickyParams = (href: string): string => {
|
|
108
|
+
if (Object.keys(stickyParams).length === 0) return href;
|
|
109
|
+
const url = new URL(href, "https://astroflare.local");
|
|
110
|
+
for (const [key, value] of Object.entries(stickyParams)) {
|
|
111
|
+
url.searchParams.set(key, value);
|
|
112
|
+
}
|
|
113
|
+
return `${url.pathname}?${url.searchParams.toString()}`;
|
|
114
|
+
};
|
|
115
|
+
const allHrefWithSticky = withStickyParams(allHref);
|
|
116
|
+
const serverFilterItemsWithSticky = serverFilterItems.map((item) => ({
|
|
117
|
+
...item,
|
|
118
|
+
href: item.href ? withStickyParams(item.href) : item.href,
|
|
119
|
+
}));
|
|
120
|
+
const pagerItemsWithSticky = pagerItems.map((item) =>
|
|
121
|
+
"href" in item && typeof item.href === "string"
|
|
122
|
+
? { ...item, href: withStickyParams(item.href) }
|
|
123
|
+
: item,
|
|
124
|
+
);
|
|
95
125
|
---
|
|
96
126
|
|
|
97
127
|
{useServer && (
|
|
@@ -100,30 +130,46 @@ const sanitizedSizeOptions = Array.from(
|
|
|
100
130
|
name="filters"
|
|
101
131
|
>
|
|
102
132
|
{serverFilterItems.length > 0 && (
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
133
|
+
<FilterPanel title={filtersTitle}>
|
|
134
|
+
<FilterRow label={filtersLabel}>
|
|
135
|
+
<FilterPills
|
|
136
|
+
items={serverFilterItemsWithSticky}
|
|
137
|
+
allHref={allHrefWithSticky}
|
|
138
|
+
active={activeTagTokens.length === 0 ? "all" : "__filtered__"}
|
|
139
|
+
class={filtersClass}
|
|
140
|
+
/>
|
|
141
|
+
</FilterRow>
|
|
142
|
+
{showExtraFilters && Astro.slots.has("extra-filters") && (
|
|
143
|
+
<FilterRow label={extraFiltersLabel}>
|
|
144
|
+
<slot name="extra-filters" />
|
|
145
|
+
</FilterRow>
|
|
146
|
+
)}
|
|
147
|
+
</FilterPanel>
|
|
109
148
|
)}
|
|
110
149
|
</slot>
|
|
111
150
|
<slot />
|
|
112
151
|
<slot name="pager">
|
|
113
|
-
<div class:list={["af-query-pager-row
|
|
114
|
-
<Pager items={
|
|
152
|
+
<div class:list={["af-query-pager-row flex items-center justify-between gap-3", pagerClass]}>
|
|
153
|
+
<Pager items={pagerItemsWithSticky} />
|
|
115
154
|
{showPageSize && (
|
|
116
155
|
<form method="get" action={pathname} data-astro-reload class:list={["af-query-size-form md:col-start-3 md:justify-self-end", pageSizeClass]}>
|
|
117
156
|
<input type="hidden" name="page" value="1" />
|
|
118
157
|
<input type="hidden" name="offset" value="0" />
|
|
158
|
+
{Object.entries(stickyParams).map(([key, value]) => (
|
|
159
|
+
<input type="hidden" name={key} value={value} />
|
|
160
|
+
))}
|
|
119
161
|
{Object.keys(activeFilters).length > 0 && (
|
|
120
162
|
<input type="hidden" name="filters" value={JSON.stringify(activeFilters)} />
|
|
121
163
|
)}
|
|
122
|
-
<label
|
|
164
|
+
<label
|
|
165
|
+
class:list={["af-page-size-label inline-flex items-center gap-2 text-xs uppercase tracking-[0.08em] text-light-text-subtle dark:text-dark-text-subtle", pageSizeDisabled && "is-disabled"]}
|
|
166
|
+
title={pageSizeDisabled ? pageSizeDisabledTitle : undefined}
|
|
167
|
+
>
|
|
123
168
|
<span>Page size</span>
|
|
124
169
|
<select
|
|
125
170
|
name="size"
|
|
126
171
|
class="select-none rounded-full border border-light-border bg-light-surface px-3 py-1.5 text-xs text-light-text-body dark:border-dark-border dark:bg-dark-surface dark:text-dark-text-body"
|
|
172
|
+
disabled={pageSizeDisabled}
|
|
127
173
|
onchange="this.form?.submit()"
|
|
128
174
|
>
|
|
129
175
|
{sanitizedSizeOptions.map((size) => (
|
|
@@ -184,4 +230,12 @@ const sanitizedSizeOptions = Array.from(
|
|
|
184
230
|
user-select: none;
|
|
185
231
|
-webkit-user-select: none;
|
|
186
232
|
}
|
|
233
|
+
.af-page-size-label.is-disabled {
|
|
234
|
+
opacity: 0.58;
|
|
235
|
+
cursor: not-allowed;
|
|
236
|
+
}
|
|
237
|
+
.af-page-size-label.is-disabled select {
|
|
238
|
+
cursor: not-allowed;
|
|
239
|
+
border-style: dashed;
|
|
240
|
+
}
|
|
187
241
|
</style>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
import FilterPills from "./FilterPills.astro";
|
|
3
|
+
|
|
4
|
+
interface DatePresetItem {
|
|
5
|
+
value: string;
|
|
6
|
+
label: string;
|
|
7
|
+
href: string;
|
|
8
|
+
active?: boolean;
|
|
9
|
+
colorKey?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
pathname: string;
|
|
14
|
+
size: number;
|
|
15
|
+
activeDateFrom?: string;
|
|
16
|
+
activeDateTo?: string;
|
|
17
|
+
baseFilters: Record<string, string>;
|
|
18
|
+
anyDateHref: string;
|
|
19
|
+
datePresets: DatePresetItem[];
|
|
20
|
+
stickyParams?: Record<string, string>;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
pathname,
|
|
26
|
+
size,
|
|
27
|
+
activeDateFrom = "",
|
|
28
|
+
activeDateTo = "",
|
|
29
|
+
baseFilters,
|
|
30
|
+
anyDateHref,
|
|
31
|
+
datePresets,
|
|
32
|
+
stickyParams = {},
|
|
33
|
+
class: className = "",
|
|
34
|
+
} = Astro.props;
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
<div class:list={["af-date-range-controls space-y-3", className]}>
|
|
38
|
+
<form
|
|
39
|
+
method="get"
|
|
40
|
+
action={pathname}
|
|
41
|
+
data-astro-reload
|
|
42
|
+
class="af-date-range-form flex flex-wrap items-end gap-2"
|
|
43
|
+
data-base-filters={JSON.stringify(baseFilters)}
|
|
44
|
+
onsubmit="const f=this;const base=JSON.parse(f.dataset.baseFilters||'{}');const from=f.querySelector('[data-date-from]')?.value?.trim();const to=f.querySelector('[data-date-to]')?.value?.trim();if(from)base.date_from=from;else delete base.date_from;if(to)base.date_to=to;else delete base.date_to;const hidden=f.querySelector('[data-filters-json]');if(Object.keys(base).length===0){hidden.removeAttribute('name');hidden.value='';}else{hidden.setAttribute('name','filters');hidden.value=JSON.stringify(base);}"
|
|
45
|
+
>
|
|
46
|
+
<input type="hidden" name="page" value="1" />
|
|
47
|
+
<input type="hidden" name="offset" value="0" />
|
|
48
|
+
<input type="hidden" name="size" value={String(size)} />
|
|
49
|
+
{Object.entries(stickyParams).map(([key, value]) => (
|
|
50
|
+
<input type="hidden" name={key} value={value} />
|
|
51
|
+
))}
|
|
52
|
+
<input type="hidden" data-filters-json value={JSON.stringify(baseFilters)} />
|
|
53
|
+
<label class="inline-flex items-center gap-2 text-[0.68rem] font-medium uppercase tracking-[0.06em] text-light-text-subtle dark:text-dark-text-subtle">
|
|
54
|
+
<span>From</span>
|
|
55
|
+
<input
|
|
56
|
+
type="date"
|
|
57
|
+
data-date-from
|
|
58
|
+
value={activeDateFrom}
|
|
59
|
+
oninput="if(this.value===''||/^\d{4}-\d{2}-\d{2}$/.test(this.value)){this.form?.requestSubmit?.() ?? this.form?.submit()}"
|
|
60
|
+
onchange="this.form?.requestSubmit?.() ?? this.form?.submit()"
|
|
61
|
+
class="af-date-chip-input rounded-full border border-light-border bg-light-surface px-2.5 py-0.5 text-[0.68rem] font-medium tracking-[0.06em] text-light-text-body dark:border-dark-border dark:bg-dark-surface dark:text-dark-text-body"
|
|
62
|
+
/>
|
|
63
|
+
</label>
|
|
64
|
+
<label class="inline-flex items-center gap-2 text-[0.68rem] font-medium uppercase tracking-[0.06em] text-light-text-subtle dark:text-dark-text-subtle">
|
|
65
|
+
<span>To</span>
|
|
66
|
+
<input
|
|
67
|
+
type="date"
|
|
68
|
+
data-date-to
|
|
69
|
+
value={activeDateTo}
|
|
70
|
+
oninput="if(this.value===''||/^\d{4}-\d{2}-\d{2}$/.test(this.value)){this.form?.requestSubmit?.() ?? this.form?.submit()}"
|
|
71
|
+
onchange="this.form?.requestSubmit?.() ?? this.form?.submit()"
|
|
72
|
+
class="af-date-chip-input rounded-full border border-light-border bg-light-surface px-2.5 py-0.5 text-[0.68rem] font-medium tracking-[0.06em] text-light-text-body dark:border-dark-border dark:bg-dark-surface dark:text-dark-text-body"
|
|
73
|
+
/>
|
|
74
|
+
</label>
|
|
75
|
+
</form>
|
|
76
|
+
<FilterPills
|
|
77
|
+
includeAll
|
|
78
|
+
allLabel="Any Date"
|
|
79
|
+
allHref={anyDateHref}
|
|
80
|
+
active={datePresets.some((item) => item.active) ? "__filtered__" : "all"}
|
|
81
|
+
itemCase="none"
|
|
82
|
+
items={datePresets}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<style is:global>
|
|
87
|
+
.af-date-range-form input[type="date"] {
|
|
88
|
+
color-scheme: light;
|
|
89
|
+
min-height: 1.625rem;
|
|
90
|
+
}
|
|
91
|
+
[data-theme="dark"] .af-date-range-form input[type="date"] {
|
|
92
|
+
color-scheme: dark;
|
|
93
|
+
}
|
|
94
|
+
.af-date-range-form input[type="date"]::-webkit-calendar-picker-indicator {
|
|
95
|
+
opacity: 0.95;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
filter: saturate(0) brightness(0.38) contrast(1.2);
|
|
98
|
+
}
|
|
99
|
+
[data-theme="dark"] .af-date-range-form input[type="date"]::-webkit-calendar-picker-indicator {
|
|
100
|
+
filter: saturate(0) brightness(1.15) contrast(1.05);
|
|
101
|
+
}
|
|
102
|
+
.af-date-range-form input[type="date"]:focus-visible {
|
|
103
|
+
outline: 2px solid color-mix(in oklab, var(--color-secondary, #7c3aed) 55%, transparent);
|
|
104
|
+
outline-offset: 2px;
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
summary: string;
|
|
4
|
+
open?: boolean;
|
|
5
|
+
class?: string;
|
|
6
|
+
summaryClass?: string;
|
|
7
|
+
contentClass?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
summary,
|
|
12
|
+
open = false,
|
|
13
|
+
class: className = "",
|
|
14
|
+
summaryClass = "",
|
|
15
|
+
contentClass = "",
|
|
16
|
+
} = Astro.props;
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<details class:list={["af-disclosure", className]} open={open}>
|
|
20
|
+
<summary class:list={["af-disclosure-summary", summaryClass]}>
|
|
21
|
+
<span>{summary}</span>
|
|
22
|
+
<span class="af-disclosure-chevron" aria-hidden="true">
|
|
23
|
+
<svg viewBox="0 0 16 16" width="16" height="16" focusable="false" aria-hidden="true">
|
|
24
|
+
<path d="M4 6.5L8 10.5L12 6.5" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" />
|
|
25
|
+
</svg>
|
|
26
|
+
</span>
|
|
27
|
+
</summary>
|
|
28
|
+
<div class:list={["af-disclosure-content", contentClass]}>
|
|
29
|
+
<slot />
|
|
30
|
+
</div>
|
|
31
|
+
</details>
|
|
32
|
+
|
|
33
|
+
<script>
|
|
34
|
+
const parseHasActiveFilters = () => {
|
|
35
|
+
const params = new URLSearchParams(window.location.search);
|
|
36
|
+
const rawFilters = params.get("filters");
|
|
37
|
+
if (!rawFilters) return false;
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(rawFilters);
|
|
40
|
+
return Boolean(parsed && typeof parsed === "object" && Object.keys(parsed).length > 0);
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const ensureStickyInput = (form) => {
|
|
47
|
+
let input = form.querySelector('input[name="filters_open"]');
|
|
48
|
+
if (!input) {
|
|
49
|
+
input = document.createElement("input");
|
|
50
|
+
input.setAttribute("type", "hidden");
|
|
51
|
+
input.setAttribute("name", "filters_open");
|
|
52
|
+
form.appendChild(input);
|
|
53
|
+
}
|
|
54
|
+
input.setAttribute("value", "1");
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const syncFiltersOpenState = (isOpen) => {
|
|
58
|
+
const hasActiveFilters = parseHasActiveFilters();
|
|
59
|
+
const shouldPersistOpen = isOpen || hasActiveFilters;
|
|
60
|
+
const forms = document.querySelectorAll('form[method="get"]');
|
|
61
|
+
|
|
62
|
+
for (const formNode of forms) {
|
|
63
|
+
if (!(formNode instanceof HTMLFormElement)) continue;
|
|
64
|
+
if (shouldPersistOpen) {
|
|
65
|
+
ensureStickyInput(formNode);
|
|
66
|
+
} else {
|
|
67
|
+
const input = formNode.querySelector('input[name="filters_open"]');
|
|
68
|
+
if (input) input.remove();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const nextUrl = new URL(window.location.href);
|
|
73
|
+
if (shouldPersistOpen) nextUrl.searchParams.set("filters_open", "1");
|
|
74
|
+
else nextUrl.searchParams.delete("filters_open");
|
|
75
|
+
if (nextUrl.toString() !== window.location.href) {
|
|
76
|
+
window.history.replaceState(window.history.state, "", nextUrl.toString());
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const disclosures = document.querySelectorAll("details.af-disclosure");
|
|
81
|
+
for (const disclosureNode of disclosures) {
|
|
82
|
+
if (!(disclosureNode instanceof HTMLDetailsElement)) continue;
|
|
83
|
+
syncFiltersOpenState(disclosureNode.open);
|
|
84
|
+
disclosureNode.addEventListener("toggle", () => {
|
|
85
|
+
syncFiltersOpenState(disclosureNode.open);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<style is:global>
|
|
91
|
+
.af-disclosure-summary {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: space-between;
|
|
95
|
+
gap: 0.5rem;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
list-style: none;
|
|
98
|
+
}
|
|
99
|
+
.af-disclosure-summary::-webkit-details-marker {
|
|
100
|
+
display: none;
|
|
101
|
+
}
|
|
102
|
+
.af-disclosure-chevron {
|
|
103
|
+
display: inline-flex;
|
|
104
|
+
width: 1.75rem;
|
|
105
|
+
height: 1.75rem;
|
|
106
|
+
align-items: center;
|
|
107
|
+
justify-content: center;
|
|
108
|
+
border-radius: 9999px;
|
|
109
|
+
border: 1px solid currentColor;
|
|
110
|
+
flex: 0 0 auto;
|
|
111
|
+
opacity: 0.88;
|
|
112
|
+
transition: transform 140ms ease;
|
|
113
|
+
transform-origin: center;
|
|
114
|
+
}
|
|
115
|
+
.af-disclosure[open] .af-disclosure-chevron {
|
|
116
|
+
transform: rotate(180deg);
|
|
117
|
+
}
|
|
118
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
title?: string;
|
|
4
|
+
class?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const { title = "Filters", class: className = "" } = Astro.props;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<section class:list={["af-filter-panel", className]} aria-label={title}>
|
|
11
|
+
{title && <p class="af-filter-panel-title">{title}</p>}
|
|
12
|
+
<div class="af-filter-panel-body">
|
|
13
|
+
<slot />
|
|
14
|
+
</div>
|
|
15
|
+
</section>
|
|
16
|
+
|
|
17
|
+
<style is:global>
|
|
18
|
+
.af-filter-panel-title {
|
|
19
|
+
margin: 0 0 0.8rem 0;
|
|
20
|
+
font-size: 0.82rem;
|
|
21
|
+
font-weight: 500;
|
|
22
|
+
letter-spacing: 0.04em;
|
|
23
|
+
text-transform: uppercase;
|
|
24
|
+
color: color-mix(in oklab, var(--color-light-text-title, #1f2937) 72%, var(--color-light-text-subtle, #6b7280));
|
|
25
|
+
}
|
|
26
|
+
[data-theme="dark"] .af-filter-panel-title {
|
|
27
|
+
color: color-mix(in oklab, var(--color-dark-text-title, #f3f4f6) 72%, var(--color-dark-text-subtle, #9ca3af));
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
@@ -112,9 +112,9 @@ const normalizedItems = items.map((item) =>
|
|
|
112
112
|
type="submit"
|
|
113
113
|
class:list={["af-filter-pill", isActive && "is-active"]}
|
|
114
114
|
style={{
|
|
115
|
-
"--af-
|
|
116
|
-
"--af-
|
|
117
|
-
"--af-
|
|
115
|
+
"--af-tag-bg": palette.bg,
|
|
116
|
+
"--af-tag-border": palette.border,
|
|
117
|
+
"--af-tag-text": palette.text,
|
|
118
118
|
}}
|
|
119
119
|
>
|
|
120
120
|
{label}
|
|
@@ -127,11 +127,12 @@ const normalizedItems = items.map((item) =>
|
|
|
127
127
|
type="button"
|
|
128
128
|
disabled
|
|
129
129
|
aria-disabled="true"
|
|
130
|
+
title="Nothing to show with current filters. Reset or remove filters."
|
|
130
131
|
class:list={["af-filter-pill", isActive && "is-active"]}
|
|
131
132
|
style={{
|
|
132
|
-
"--af-
|
|
133
|
-
"--af-
|
|
134
|
-
"--af-
|
|
133
|
+
"--af-tag-bg": palette.bg,
|
|
134
|
+
"--af-tag-border": palette.border,
|
|
135
|
+
"--af-tag-text": palette.text,
|
|
135
136
|
}}
|
|
136
137
|
>
|
|
137
138
|
{label}
|
|
@@ -142,9 +143,9 @@ const normalizedItems = items.map((item) =>
|
|
|
142
143
|
data-filter={item.value}
|
|
143
144
|
class:list={["af-filter-pill", isActive && "is-active"]}
|
|
144
145
|
style={{
|
|
145
|
-
"--af-
|
|
146
|
-
"--af-
|
|
147
|
-
"--af-
|
|
146
|
+
"--af-tag-bg": palette.bg,
|
|
147
|
+
"--af-tag-border": palette.border,
|
|
148
|
+
"--af-tag-text": palette.text,
|
|
148
149
|
}}
|
|
149
150
|
>
|
|
150
151
|
{label}
|
|
@@ -157,13 +158,13 @@ const normalizedItems = items.map((item) =>
|
|
|
157
158
|
<style>
|
|
158
159
|
.af-filter-pill {
|
|
159
160
|
border-radius: 9999px;
|
|
160
|
-
border: 1px solid var(--af-
|
|
161
|
-
background: var(--af-
|
|
162
|
-
color: var(--af-
|
|
163
|
-
padding: 0.
|
|
164
|
-
font-size: 0.
|
|
161
|
+
border: 1px solid var(--af-tag-border, var(--color-light-border));
|
|
162
|
+
background: var(--af-tag-bg, color-mix(in oklab, var(--color-light-surface) 80%, transparent));
|
|
163
|
+
color: var(--af-tag-text, color-mix(in oklab, var(--color-light-text-body, #111827) 88%, transparent));
|
|
164
|
+
padding: 0.125rem 0.625rem;
|
|
165
|
+
font-size: 0.68rem;
|
|
165
166
|
font-weight: 500;
|
|
166
|
-
letter-spacing: 0.
|
|
167
|
+
letter-spacing: 0.06em;
|
|
167
168
|
text-transform: uppercase;
|
|
168
169
|
cursor: pointer;
|
|
169
170
|
transition: transform 130ms ease, filter 130ms ease;
|
|
@@ -179,19 +180,20 @@ const normalizedItems = items.map((item) =>
|
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
.af-filter-pill:hover {
|
|
182
|
-
filter: brightness(1.04);
|
|
183
|
+
filter: brightness(0.97) saturate(1.04);
|
|
183
184
|
}
|
|
184
185
|
.af-filter-pill:disabled,
|
|
185
186
|
.af-filter-pill[aria-disabled="true"] {
|
|
186
187
|
cursor: not-allowed;
|
|
187
|
-
opacity: 0.
|
|
188
|
-
filter: grayscale(0.
|
|
188
|
+
opacity: 0.34;
|
|
189
|
+
filter: grayscale(0.45) contrast(0.86);
|
|
190
|
+
color: color-mix(in oklab, var(--af-tag-text, var(--color-light-text-subtle, #6b7280)) 42%, var(--color-light-text-subtle, #6b7280));
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
.af-filter-pill.is-active {
|
|
192
|
-
border-color: color-mix(in oklab, var(--af-
|
|
193
|
-
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--af-
|
|
194
|
-
outline: 2px solid color-mix(in oklab, var(--af-
|
|
194
|
+
border-color: color-mix(in oklab, var(--af-tag-text, var(--color-secondary)) 80%, transparent);
|
|
195
|
+
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--af-tag-text, var(--color-secondary)) 36%, transparent);
|
|
196
|
+
outline: 2px solid color-mix(in oklab, var(--af-tag-text, var(--color-secondary)) 48%, transparent);
|
|
195
197
|
outline-offset: 1px;
|
|
196
198
|
font-weight: 600;
|
|
197
199
|
filter: saturate(1.14) brightness(1.06);
|
|
@@ -200,28 +202,34 @@ const normalizedItems = items.map((item) =>
|
|
|
200
202
|
text-underline-offset: 0.15em;
|
|
201
203
|
}
|
|
202
204
|
.af-filter-pill.af-filter-pill-all {
|
|
203
|
-
--af-
|
|
204
|
-
--af-
|
|
205
|
-
--af-
|
|
205
|
+
--af-tag-bg: transparent;
|
|
206
|
+
--af-tag-border: color-mix(in oklab, var(--color-light-text-subtle) 45%, transparent);
|
|
207
|
+
--af-tag-text: color-mix(in oklab, var(--color-light-text-body) 92%, transparent);
|
|
206
208
|
}
|
|
207
209
|
.af-filter-pill.af-filter-pill-all.is-active {
|
|
208
|
-
--af-
|
|
209
|
-
--af-
|
|
210
|
-
--af-
|
|
210
|
+
--af-tag-bg: color-mix(in oklab, var(--color-light-text-body) 7%, transparent);
|
|
211
|
+
--af-tag-border: color-mix(in oklab, var(--color-light-text-body) 72%, transparent);
|
|
212
|
+
--af-tag-text: var(--color-light-text-body);
|
|
211
213
|
}
|
|
212
214
|
:global([data-theme="dark"]) .af-filter-pill {
|
|
213
|
-
color: var(--af-
|
|
214
|
-
border-color: var(--af-
|
|
215
|
-
background: var(--af-
|
|
215
|
+
color: color-mix(in oklab, var(--af-tag-text, #d1d5db) 34%, var(--color-dark-text-body, #f3f4f6));
|
|
216
|
+
border-color: color-mix(in oklab, var(--af-tag-border, var(--color-dark-border)) 62%, var(--color-dark-border) 38%);
|
|
217
|
+
background: color-mix(in oklab, var(--af-tag-bg, transparent) 48%, var(--color-dark-surface, #1f2937) 52%);
|
|
216
218
|
}
|
|
217
219
|
:global([data-theme="dark"]) .af-filter-pill.af-filter-pill-all {
|
|
218
|
-
--af-
|
|
219
|
-
--af-
|
|
220
|
-
--af-
|
|
220
|
+
--af-tag-bg: transparent;
|
|
221
|
+
--af-tag-border: color-mix(in oklab, var(--color-dark-text-subtle) 48%, transparent);
|
|
222
|
+
--af-tag-text: color-mix(in oklab, var(--color-dark-text-body) 90%, transparent);
|
|
221
223
|
}
|
|
222
224
|
:global([data-theme="dark"]) .af-filter-pill.af-filter-pill-all.is-active {
|
|
223
|
-
--af-
|
|
224
|
-
--af-
|
|
225
|
-
--af-
|
|
225
|
+
--af-tag-bg: color-mix(in oklab, var(--color-dark-text-body) 10%, transparent);
|
|
226
|
+
--af-tag-border: color-mix(in oklab, var(--color-dark-text-body) 70%, transparent);
|
|
227
|
+
--af-tag-text: var(--color-dark-text-body);
|
|
228
|
+
}
|
|
229
|
+
:global([data-theme="dark"]) .af-filter-pill:disabled,
|
|
230
|
+
:global([data-theme="dark"]) .af-filter-pill[aria-disabled="true"] {
|
|
231
|
+
opacity: 0.42;
|
|
232
|
+
filter: grayscale(0.35) contrast(0.9);
|
|
233
|
+
color: color-mix(in oklab, var(--color-dark-text-subtle, #9ca3af) 68%, var(--af-tag-text, #d1d5db));
|
|
226
234
|
}
|
|
227
235
|
</style>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
label: string;
|
|
4
|
+
class?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const { label, class: className = "" } = Astro.props;
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<div class:list={["af-filter-row", className]}>
|
|
11
|
+
<p class="af-filter-row-label">{label}</p>
|
|
12
|
+
<div class="af-filter-row-content">
|
|
13
|
+
<slot />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<style is:global>
|
|
18
|
+
.af-filter-row {
|
|
19
|
+
display: grid;
|
|
20
|
+
grid-template-columns: 4.75rem 1fr;
|
|
21
|
+
align-items: start;
|
|
22
|
+
column-gap: 0.75rem;
|
|
23
|
+
}
|
|
24
|
+
.af-filter-row + .af-filter-row {
|
|
25
|
+
margin-top: 0.95rem;
|
|
26
|
+
padding-top: 0.9rem;
|
|
27
|
+
border-top: 1px solid color-mix(in oklab, var(--color-area-divider-light, var(--color-light-border, #d1d5db)) 58%, transparent);
|
|
28
|
+
}
|
|
29
|
+
.af-filter-row-label {
|
|
30
|
+
margin: 0;
|
|
31
|
+
padding-top: 0.35rem;
|
|
32
|
+
font-size: 0.75rem;
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
letter-spacing: 0.08em;
|
|
35
|
+
text-transform: uppercase;
|
|
36
|
+
color: var(--color-area-ui-label-light, color-mix(in oklab, var(--color-light-text-subtle, #6b7280) 92%, transparent));
|
|
37
|
+
}
|
|
38
|
+
.af-filter-row-content {
|
|
39
|
+
min-width: 0;
|
|
40
|
+
padding-bottom: 0.15rem;
|
|
41
|
+
}
|
|
42
|
+
[data-theme="dark"] .af-filter-row-label {
|
|
43
|
+
color: var(--color-area-ui-label-dark, color-mix(in oklab, var(--color-dark-text-body, #f3f4f6) 68%, var(--color-dark-text-subtle, #9ca3af) 32%));
|
|
44
|
+
}
|
|
45
|
+
[data-theme="dark"] .af-filter-row + .af-filter-row {
|
|
46
|
+
border-top-color: color-mix(in oklab, var(--color-area-divider-dark, var(--color-dark-border, #374151)) 58%, transparent);
|
|
47
|
+
}
|
|
48
|
+
@media (max-width: 640px) {
|
|
49
|
+
.af-filter-row {
|
|
50
|
+
grid-template-columns: 1fr;
|
|
51
|
+
row-gap: 0.35rem;
|
|
52
|
+
}
|
|
53
|
+
.af-filter-row-label {
|
|
54
|
+
padding-top: 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
</style>
|
|
@@ -53,6 +53,7 @@ const formPartsFromHref = (href: string): { action: string; fields: Array<{ name
|
|
|
53
53
|
data-page={String(item.page)}
|
|
54
54
|
disabled={item.disabled ?? item.active}
|
|
55
55
|
aria-disabled={item.disabled ?? item.active}
|
|
56
|
+
title={(item.disabled ?? item.active) ? "Unavailable ⛔" : undefined}
|
|
56
57
|
class:list={["af-pager-pill", item.active && "is-active"]}
|
|
57
58
|
>
|
|
58
59
|
{item.label ?? item.page}
|
|
@@ -97,7 +98,6 @@ const formPartsFromHref = (href: string): { action: string; fields: Array<{ name
|
|
|
97
98
|
.af-pager .af-pager-pill[aria-disabled="true"] {
|
|
98
99
|
cursor: not-allowed;
|
|
99
100
|
opacity: 0.5;
|
|
100
|
-
pointer-events: none;
|
|
101
101
|
border-color: color-mix(in oklab, var(--color-light-border) 65%, transparent);
|
|
102
102
|
color: color-mix(in oklab, var(--color-light-text-subtle) 50%, transparent);
|
|
103
103
|
background: color-mix(in oklab, var(--color-light-surface) 35%, transparent);
|
|
@@ -43,12 +43,12 @@ const overflowTooltip = overflowItems.map(formatItem).join(", ");
|
|
|
43
43
|
return (
|
|
44
44
|
<span
|
|
45
45
|
style={{
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
"--af-tag-bg": palette.bg,
|
|
47
|
+
"--af-tag-border": palette.border,
|
|
48
|
+
"--af-tag-text": palette.text,
|
|
49
49
|
}}
|
|
50
50
|
class:list={[
|
|
51
|
-
"inline-block rounded-full border px-2.5 py-0.5 text-[0.68rem] font-medium uppercase tracking-[0.06em]",
|
|
51
|
+
"af-tag-tone inline-block rounded-full border px-2.5 py-0.5 text-[0.68rem] font-medium uppercase tracking-[0.06em]",
|
|
52
52
|
itemClass,
|
|
53
53
|
]}
|
|
54
54
|
>
|
|
@@ -69,3 +69,16 @@ const overflowTooltip = overflowItems.map(formatItem).join(", ");
|
|
|
69
69
|
</Tooltip>
|
|
70
70
|
)}
|
|
71
71
|
</div>
|
|
72
|
+
|
|
73
|
+
<style is:global>
|
|
74
|
+
.af-tag-tone {
|
|
75
|
+
background: var(--af-tag-bg, color-mix(in oklab, var(--color-light-surface) 86%, transparent));
|
|
76
|
+
border-color: var(--af-tag-border, color-mix(in oklab, var(--color-light-border) 72%, transparent));
|
|
77
|
+
color: var(--af-tag-text, color-mix(in oklab, var(--color-light-text-title) 84%, var(--color-light-text-subtle) 16%));
|
|
78
|
+
}
|
|
79
|
+
[data-theme="dark"] .af-tag-tone {
|
|
80
|
+
background: color-mix(in oklab, var(--af-tag-bg, transparent) 48%, var(--color-dark-surface) 52%);
|
|
81
|
+
border-color: color-mix(in oklab, var(--af-tag-border, var(--color-dark-border)) 62%, var(--color-dark-border) 38%);
|
|
82
|
+
color: color-mix(in oklab, var(--af-tag-text, var(--color-dark-text-body)) 38%, var(--color-dark-text-body) 62%);
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
@@ -7,6 +7,16 @@ export interface CollectionQueryFilterOption {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
interface CollectionQueryBaseProps {
|
|
10
|
+
/** Extra query params to preserve across filter/pager/size interactions. */
|
|
11
|
+
stickyParams?: Record<string, string>;
|
|
12
|
+
/** Optional title shown above filters block in server mode. */
|
|
13
|
+
filtersTitle?: string;
|
|
14
|
+
/** Label shown left of tag pills in server mode. */
|
|
15
|
+
filtersLabel?: string;
|
|
16
|
+
/** Label shown left of optional extra filters slot in server mode. */
|
|
17
|
+
extraFiltersLabel?: string;
|
|
18
|
+
/** Controls whether the optional extra-filters row renders. */
|
|
19
|
+
showExtraFilters?: boolean;
|
|
10
20
|
/** Available filters for filter pill rendering. */
|
|
11
21
|
filters?: CollectionQueryFilterOption[];
|
|
12
22
|
/** Max visible pager buttons before ellipsis. */
|
package/src/tag-colors.ts
CHANGED
|
@@ -13,11 +13,11 @@ export interface TagColorOptions {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const DEFAULT_OVERRIDES: Record<string, string> = {
|
|
16
|
-
shoots: "#
|
|
17
|
-
tech: "#
|
|
18
|
-
ai: "#
|
|
19
|
-
sites: "#
|
|
20
|
-
flow: "#
|
|
16
|
+
shoots: "#c4786a",
|
|
17
|
+
tech: "#4d8c7a",
|
|
18
|
+
ai: "#d9773a",
|
|
19
|
+
sites: "#b8942e",
|
|
20
|
+
flow: "#9b6bc9",
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
const hashStringToHue = (input: string): number => {
|
|
@@ -82,8 +82,8 @@ export const getTagPalette = (
|
|
|
82
82
|
): TagPalette => {
|
|
83
83
|
const hue = resolveHue(tag, options);
|
|
84
84
|
return {
|
|
85
|
-
bg: `hsla(${hue}
|
|
86
|
-
border: `hsla(${hue}
|
|
87
|
-
text: `hsl(${hue}
|
|
85
|
+
bg: `hsla(${hue} 58% 56% / 0.16)`,
|
|
86
|
+
border: `hsla(${hue} 56% 46% / 0.42)`,
|
|
87
|
+
text: `hsl(${hue} 58% 32%)`,
|
|
88
88
|
};
|
|
89
89
|
};
|