@meith/theme-raidframe 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/slots/search-form.tsx +58 -1
- package/src/slots/search-results.tsx +125 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/theme-raidframe",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A gaming theme for Meith — guild, raid and match boards — built on the default theme's slots.",
|
|
5
5
|
"license": "LGPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@meith/theme-default": "^0.
|
|
24
|
-
"@meith/
|
|
25
|
-
"@meith/
|
|
23
|
+
"@meith/theme-default": "^0.4.0",
|
|
24
|
+
"@meith/theme-kit": "^0.4.0",
|
|
25
|
+
"@meith/ui": "^0.4.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": "^19.2.0"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OptionModel, SearchFormModel } from '@meith/theme-kit'
|
|
1
|
+
import type { OptionModel, SearchAdvancedModel, SearchFormModel } from '@meith/theme-kit'
|
|
2
2
|
|
|
3
3
|
import { BUTTON_PRIMARY, Frame, MICRO, PanelHead } from '../shared'
|
|
4
4
|
|
|
@@ -34,6 +34,60 @@ function Choice({
|
|
|
34
34
|
)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function Advanced({ label, isOpen, author, toggles, choices }: SearchAdvancedModel) {
|
|
38
|
+
return (
|
|
39
|
+
<details open={isOpen} className="border border-border">
|
|
40
|
+
<summary className={`${MICRO} cursor-default px-3 py-2 select-none`}>{label}</summary>
|
|
41
|
+
|
|
42
|
+
<div className="flex flex-col gap-4 border-t border-border px-3 py-3">
|
|
43
|
+
<div>
|
|
44
|
+
<label htmlFor="search-author" className={`${MICRO} mb-1 block`}>
|
|
45
|
+
{author.label}
|
|
46
|
+
</label>
|
|
47
|
+
<input
|
|
48
|
+
id="search-author"
|
|
49
|
+
type="text"
|
|
50
|
+
name={author.field}
|
|
51
|
+
defaultValue={author.value}
|
|
52
|
+
placeholder={author.placeholder}
|
|
53
|
+
autoComplete="off"
|
|
54
|
+
aria-describedby="search-author-hint"
|
|
55
|
+
className={CONTROL}
|
|
56
|
+
/>
|
|
57
|
+
<p id="search-author-hint" className="mt-1.5 text-xs text-muted-foreground">
|
|
58
|
+
{author.hint}
|
|
59
|
+
</p>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{toggles.map((toggle) => (
|
|
63
|
+
<label key={toggle.field} className="flex items-center gap-2 text-sm select-none">
|
|
64
|
+
<input
|
|
65
|
+
type="checkbox"
|
|
66
|
+
name={toggle.field}
|
|
67
|
+
value={toggle.value}
|
|
68
|
+
defaultChecked={toggle.isOn}
|
|
69
|
+
className="size-4 border border-input accent-primary"
|
|
70
|
+
/>
|
|
71
|
+
{toggle.label}
|
|
72
|
+
</label>
|
|
73
|
+
))}
|
|
74
|
+
|
|
75
|
+
<div className="grid gap-4 sm:grid-cols-3">
|
|
76
|
+
{choices.map((choice) => (
|
|
77
|
+
<Choice
|
|
78
|
+
key={choice.field}
|
|
79
|
+
id={`search-${choice.field}`}
|
|
80
|
+
label={choice.label}
|
|
81
|
+
name={choice.field}
|
|
82
|
+
options={choice.options}
|
|
83
|
+
/>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</details>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
37
91
|
export function SearchForm({
|
|
38
92
|
action,
|
|
39
93
|
fields,
|
|
@@ -43,6 +97,7 @@ export function SearchForm({
|
|
|
43
97
|
sorts,
|
|
44
98
|
hint,
|
|
45
99
|
errorMessage,
|
|
100
|
+
advanced,
|
|
46
101
|
}: SearchFormModel) {
|
|
47
102
|
const described = [
|
|
48
103
|
hint === null ? null : 'search-hint',
|
|
@@ -91,6 +146,8 @@ export function SearchForm({
|
|
|
91
146
|
<Choice id="search-sort" label="Sort by" name={fields.sort} options={sorts} />
|
|
92
147
|
</div>
|
|
93
148
|
|
|
149
|
+
{advanced !== undefined && <Advanced {...advanced} />}
|
|
150
|
+
|
|
94
151
|
<div>
|
|
95
152
|
<button type="submit" className={BUTTON_PRIMARY}>
|
|
96
153
|
search
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { SearchResultsModel } from '@meith/theme-kit'
|
|
1
|
+
import type { OptionModel, SearchRefineModel, SearchResultsModel } from '@meith/theme-kit'
|
|
2
2
|
|
|
3
3
|
import { BUTTON_PRIMARY, Frame, HEADING, MICRO, RULE, Stamp } from '../shared'
|
|
4
4
|
|
|
5
|
+
const CONTROL =
|
|
6
|
+
'w-full border border-input bg-surface px-3 py-2 text-sm text-foreground focus-visible:border-ring'
|
|
7
|
+
|
|
5
8
|
export function SearchResults({
|
|
6
9
|
terms,
|
|
7
10
|
searchedAt,
|
|
@@ -10,6 +13,7 @@ export function SearchResults({
|
|
|
10
13
|
nextLabel,
|
|
11
14
|
newSearchHref,
|
|
12
15
|
within,
|
|
16
|
+
refine,
|
|
13
17
|
}: SearchResultsModel) {
|
|
14
18
|
return (
|
|
15
19
|
<main
|
|
@@ -32,6 +36,8 @@ export function SearchResults({
|
|
|
32
36
|
<div className={RULE} aria-hidden="true" />
|
|
33
37
|
</div>
|
|
34
38
|
|
|
39
|
+
{refine !== undefined && <Refine {...refine} />}
|
|
40
|
+
|
|
35
41
|
<Frame>
|
|
36
42
|
{hits.length === 0 ? (
|
|
37
43
|
<div className="px-4 py-6 text-center">
|
|
@@ -72,6 +78,15 @@ export function SearchResults({
|
|
|
72
78
|
|
|
73
79
|
<Frame>
|
|
74
80
|
<form method="get" action={within.action} className="flex flex-col gap-3 px-4 py-4">
|
|
81
|
+
{(within.hidden ?? []).map((field) => (
|
|
82
|
+
<input
|
|
83
|
+
key={`${field.name}-${field.value}`}
|
|
84
|
+
type="hidden"
|
|
85
|
+
name={field.name}
|
|
86
|
+
value={field.value}
|
|
87
|
+
/>
|
|
88
|
+
))}
|
|
89
|
+
|
|
75
90
|
<div>
|
|
76
91
|
<label htmlFor="search-within" className={`${MICRO} mb-1 block`}>
|
|
77
92
|
{within.label}
|
|
@@ -83,7 +98,7 @@ export function SearchResults({
|
|
|
83
98
|
defaultValue={within.value}
|
|
84
99
|
autoComplete="off"
|
|
85
100
|
aria-describedby="search-within-hint"
|
|
86
|
-
className=
|
|
101
|
+
className={CONTROL}
|
|
87
102
|
/>
|
|
88
103
|
<p id="search-within-hint" className="mt-1.5 text-xs text-muted-foreground">
|
|
89
104
|
{within.hint}
|
|
@@ -106,3 +121,111 @@ export function SearchResults({
|
|
|
106
121
|
</main>
|
|
107
122
|
)
|
|
108
123
|
}
|
|
124
|
+
|
|
125
|
+
function Refine({
|
|
126
|
+
action,
|
|
127
|
+
label,
|
|
128
|
+
summary,
|
|
129
|
+
note,
|
|
130
|
+
sorts,
|
|
131
|
+
sortsLabel,
|
|
132
|
+
choices,
|
|
133
|
+
submitLabel,
|
|
134
|
+
applied,
|
|
135
|
+
clearHref,
|
|
136
|
+
}: SearchRefineModel) {
|
|
137
|
+
return (
|
|
138
|
+
<section
|
|
139
|
+
aria-label={label}
|
|
140
|
+
className="flex flex-col gap-2.5 border border-border bg-card px-4 py-3 shadow-elevation"
|
|
141
|
+
>
|
|
142
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
|
|
143
|
+
<p className="text-sm font-medium text-foreground">{summary}</p>
|
|
144
|
+
|
|
145
|
+
<nav aria-label={sortsLabel} className="flex items-center gap-1">
|
|
146
|
+
{sorts.map((sort) => (
|
|
147
|
+
<a
|
|
148
|
+
key={sort.label}
|
|
149
|
+
href={sort.href}
|
|
150
|
+
{...(sort.isCurrent ? { 'aria-current': 'true' as const } : {})}
|
|
151
|
+
className={`${MICRO} border px-2 py-1 ${
|
|
152
|
+
sort.isCurrent
|
|
153
|
+
? 'border-primary bg-primary/10 text-primary'
|
|
154
|
+
: 'border-transparent hover:text-primary'
|
|
155
|
+
}`}
|
|
156
|
+
>
|
|
157
|
+
{sort.label}
|
|
158
|
+
</a>
|
|
159
|
+
))}
|
|
160
|
+
</nav>
|
|
161
|
+
|
|
162
|
+
{applied.map((chip) => (
|
|
163
|
+
<a
|
|
164
|
+
key={chip.label}
|
|
165
|
+
href={chip.removeHref}
|
|
166
|
+
className={`${MICRO} inline-flex items-center gap-1.5 border border-primary bg-primary/10 px-2 py-1 normal-case text-primary hover:bg-primary/20`}
|
|
167
|
+
>
|
|
168
|
+
{chip.label}
|
|
169
|
+
<span aria-hidden="true">×</span>
|
|
170
|
+
<span className="sr-only">— remove this filter</span>
|
|
171
|
+
</a>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<form
|
|
176
|
+
method="get"
|
|
177
|
+
action={action}
|
|
178
|
+
className="flex flex-wrap items-center gap-x-3 gap-y-2 border-t border-border pt-2.5"
|
|
179
|
+
>
|
|
180
|
+
{choices.map((choice) => (
|
|
181
|
+
<RefineChoice key={choice.field} {...choice} />
|
|
182
|
+
))}
|
|
183
|
+
|
|
184
|
+
<button type="submit" className={`${BUTTON_PRIMARY} h-8 px-3`}>
|
|
185
|
+
{submitLabel}
|
|
186
|
+
</button>
|
|
187
|
+
|
|
188
|
+
{clearHref !== null && (
|
|
189
|
+
<a href={clearHref} className={`${MICRO} hover:text-primary`}>
|
|
190
|
+
clear filters
|
|
191
|
+
</a>
|
|
192
|
+
)}
|
|
193
|
+
</form>
|
|
194
|
+
|
|
195
|
+
{note !== null && <p className="text-xs text-muted-foreground">{note}</p>}
|
|
196
|
+
</section>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function RefineChoice({
|
|
201
|
+
field,
|
|
202
|
+
label,
|
|
203
|
+
options,
|
|
204
|
+
}: {
|
|
205
|
+
field: string
|
|
206
|
+
label: string
|
|
207
|
+
options: readonly OptionModel[]
|
|
208
|
+
}) {
|
|
209
|
+
const selected = options.find((option) => option.isSelected)
|
|
210
|
+
const id = `search-refine-${field}`
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<span className="inline-flex items-center gap-1.5">
|
|
214
|
+
<label htmlFor={id} className={MICRO}>
|
|
215
|
+
{label}
|
|
216
|
+
</label>
|
|
217
|
+
<select
|
|
218
|
+
id={id}
|
|
219
|
+
name={field}
|
|
220
|
+
defaultValue={selected?.value ?? ''}
|
|
221
|
+
className="h-8 border border-input bg-surface px-2 text-xs text-foreground focus-visible:border-ring"
|
|
222
|
+
>
|
|
223
|
+
{options.map((option) => (
|
|
224
|
+
<option key={option.value} value={option.value}>
|
|
225
|
+
{option.label}
|
|
226
|
+
</option>
|
|
227
|
+
))}
|
|
228
|
+
</select>
|
|
229
|
+
</span>
|
|
230
|
+
)
|
|
231
|
+
}
|