@meith/theme-default 0.3.2 → 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-default",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Meith's default theme — every slot and every token, and the reference for what a complete theme fills.",
5
5
  "license": "LGPL-3.0-or-later",
6
6
  "repository": {
@@ -20,8 +20,8 @@
20
20
  "access": "public"
21
21
  },
22
22
  "dependencies": {
23
- "@meith/theme-kit": "^0.3.2",
24
- "@meith/ui": "^0.3.2"
23
+ "@meith/theme-kit": "^0.4.0",
24
+ "@meith/ui": "^0.4.0"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "react": "^19.2.0"
@@ -1,16 +1,18 @@
1
1
  import { cn } from '@meith/ui'
2
2
  import type { PanelPageModel, PanelSectionModel } from '@meith/theme-kit'
3
3
 
4
- import { MUTED_LINK } from '../shared'
4
+ import { MUTED_LINK, PAGE, pageAt } from '../shared'
5
5
 
6
- export function PanelPage({ title, back, width, gap, regions, children }: PanelPageModel) {
6
+ export function PanelPage({ title, back, frame, width, gap, regions, children }: PanelPageModel) {
7
7
  return (
8
8
  <main
9
9
  id="board-content"
10
10
  tabIndex={-1}
11
11
  className={cn(
12
- 'flex w-full flex-col px-6 py-8',
13
- width === 'wide' ? 'max-w-none' : 'max-w-4xl',
12
+ 'flex w-full flex-col py-8',
13
+ frame === 'standalone'
14
+ ? `flex-1 ${width === 'wide' ? PAGE : pageAt('max-w-4xl')}`
15
+ : `px-6 ${width === 'wide' ? 'max-w-none' : 'max-w-4xl'}`,
14
16
  gap === 'loose' ? 'gap-8' : 'gap-6',
15
17
  )}
16
18
  >
@@ -4,12 +4,13 @@ import {
4
4
  AlertTitle,
5
5
  Card,
6
6
  CardContent,
7
+ Disclosure,
7
8
  Field,
8
9
  Input,
9
10
  NativeSelect,
10
11
  buttonVariants,
11
12
  } from '@meith/ui'
12
- import type { OptionModel, SearchFormModel } from '@meith/theme-kit'
13
+ import type { OptionModel, SearchAdvancedModel, SearchFormModel } from '@meith/theme-kit'
13
14
 
14
15
  export function SearchForm({
15
16
  action,
@@ -20,6 +21,7 @@ export function SearchForm({
20
21
  sorts,
21
22
  hint,
22
23
  errorMessage,
24
+ advanced,
23
25
  }: SearchFormModel) {
24
26
  return (
25
27
  <Card>
@@ -48,6 +50,8 @@ export function SearchForm({
48
50
  <Choice label="Sort by" name={fields.sort} options={sorts} />
49
51
  </div>
50
52
 
53
+ {advanced !== undefined && <Advanced {...advanced} />}
54
+
51
55
  <div>
52
56
  <button type="submit" className={buttonVariants({ variant: 'primary' })}>
53
57
  Search
@@ -67,6 +71,52 @@ export function SearchForm({
67
71
  )
68
72
  }
69
73
 
74
+ function Advanced({ label, isOpen, author, toggles, choices }: SearchAdvancedModel) {
75
+ return (
76
+ <Disclosure summary={label} open={isOpen}>
77
+ <div className="flex flex-col gap-4">
78
+ <Field name={author.field} label={author.label} description={author.hint}>
79
+ {(control) => (
80
+ <Input
81
+ {...control}
82
+ defaultValue={author.value}
83
+ placeholder={author.placeholder}
84
+ autoComplete="off"
85
+ />
86
+ )}
87
+ </Field>
88
+
89
+ {toggles.map((toggle) => (
90
+ <label
91
+ key={toggle.field}
92
+ className="flex items-center gap-2 text-sm text-foreground select-none"
93
+ >
94
+ <input
95
+ type="checkbox"
96
+ name={toggle.field}
97
+ value={toggle.value}
98
+ defaultChecked={toggle.isOn}
99
+ className="size-4 rounded border border-input accent-primary"
100
+ />
101
+ {toggle.label}
102
+ </label>
103
+ ))}
104
+
105
+ <div className="grid gap-4 sm:grid-cols-3">
106
+ {choices.map((choice) => (
107
+ <Choice
108
+ key={choice.field}
109
+ label={choice.label}
110
+ name={choice.field}
111
+ options={choice.options}
112
+ />
113
+ ))}
114
+ </div>
115
+ </div>
116
+ </Disclosure>
117
+ )
118
+ }
119
+
70
120
  function Choice({
71
121
  label,
72
122
  name,
@@ -8,9 +8,11 @@ import {
8
8
  EmptyTitle,
9
9
  Field,
10
10
  Input,
11
+ NativeSelect,
11
12
  buttonVariants,
13
+ cn,
12
14
  } from '@meith/ui'
13
- import type { SearchResultsModel } from '@meith/theme-kit'
15
+ import type { OptionModel, SearchRefineModel, SearchResultsModel } from '@meith/theme-kit'
14
16
 
15
17
  import { LINK, Stamp, pageAt } from '../shared'
16
18
 
@@ -22,6 +24,7 @@ export function SearchResults({
22
24
  nextLabel,
23
25
  newSearchHref,
24
26
  within,
27
+ refine,
25
28
  }: SearchResultsModel) {
26
29
  return (
27
30
  <main
@@ -39,6 +42,8 @@ export function SearchResults({
39
42
  </p>
40
43
  </div>
41
44
 
45
+ {refine !== undefined && <Refine {...refine} />}
46
+
42
47
  <Card>
43
48
  {hits.length === 0 ? (
44
49
  <Empty>
@@ -78,6 +83,15 @@ export function SearchResults({
78
83
  <Card>
79
84
  <CardContent className="p-4 sm:p-5">
80
85
  <form method="get" action={within.action} className="flex flex-col gap-4">
86
+ {(within.hidden ?? []).map((field) => (
87
+ <input
88
+ key={`${field.name}-${field.value}`}
89
+ type="hidden"
90
+ name={field.name}
91
+ value={field.value}
92
+ />
93
+ ))}
94
+
81
95
  <Field name={within.field} label={within.label} description={within.hint}>
82
96
  {(control) => (
83
97
  <Input
@@ -104,3 +118,122 @@ export function SearchResults({
104
118
  </main>
105
119
  )
106
120
  }
121
+
122
+ function Refine({
123
+ action,
124
+ label,
125
+ summary,
126
+ note,
127
+ sorts,
128
+ sortsLabel,
129
+ choices,
130
+ submitLabel,
131
+ applied,
132
+ clearHref,
133
+ }: SearchRefineModel) {
134
+ return (
135
+ <section
136
+ aria-label={label}
137
+ className="flex flex-col gap-2.5 rounded-lg border border-border bg-card px-3 py-2.5"
138
+ >
139
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-2">
140
+ <p className="text-sm font-medium text-foreground">{summary}</p>
141
+
142
+ <nav
143
+ aria-label={sortsLabel}
144
+ className="flex items-center rounded-md border border-border p-0.5"
145
+ >
146
+ {sorts.map((sort) => (
147
+ <a
148
+ key={sort.label}
149
+ href={sort.href}
150
+ {...(sort.isCurrent ? { 'aria-current': 'true' as const } : {})}
151
+ className={`rounded px-2.5 py-1 text-xs font-medium ${
152
+ sort.isCurrent
153
+ ? 'bg-secondary text-foreground'
154
+ : 'text-muted-foreground hover:bg-muted hover:text-foreground'
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="inline-flex items-center gap-1.5 rounded-full border border-primary/40 bg-primary/10 px-2.5 py-1 text-xs font-medium text-foreground 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
+ <Filter
182
+ key={choice.field}
183
+ label={choice.label}
184
+ name={choice.field}
185
+ options={choice.options}
186
+ />
187
+ ))}
188
+
189
+ <button
190
+ type="submit"
191
+ className={cn(buttonVariants({ variant: 'secondary', size: 'sm' }), 'h-8')}
192
+ >
193
+ {submitLabel}
194
+ </button>
195
+
196
+ {clearHref !== null && (
197
+ <a href={clearHref} className={`text-xs font-medium text-foreground ${LINK}`}>
198
+ Clear filters
199
+ </a>
200
+ )}
201
+ </form>
202
+
203
+ {note !== null && <p className="text-xs text-muted-foreground">{note}</p>}
204
+ </section>
205
+ )
206
+ }
207
+
208
+ function Filter({
209
+ label,
210
+ name,
211
+ options,
212
+ }: {
213
+ label: string
214
+ name: string
215
+ options: readonly OptionModel[]
216
+ }) {
217
+ const selected = options.find((option) => option.isSelected)
218
+ const id = `filter-${name}`
219
+
220
+ return (
221
+ <span className="inline-flex items-center gap-1.5">
222
+ <label htmlFor={id} className="text-xs font-medium text-muted-foreground">
223
+ {label}
224
+ </label>
225
+ <NativeSelect
226
+ id={id}
227
+ name={name}
228
+ defaultValue={selected?.value ?? ''}
229
+ className="h-8 w-auto min-w-0 py-0 text-xs"
230
+ >
231
+ {options.map((option) => (
232
+ <option key={option.value} value={option.value}>
233
+ {option.label}
234
+ </option>
235
+ ))}
236
+ </NativeSelect>
237
+ </span>
238
+ )
239
+ }