@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meith/theme-raidframe",
3
- "version": "0.3.3",
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.3.3",
24
- "@meith/ui": "^0.3.3",
25
- "@meith/theme-kit": "^0.3.3"
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="w-full border border-input bg-surface px-3 py-2 text-sm text-foreground focus-visible:border-ring"
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
+ }