@meith/theme-phasebook 0.3.0 → 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-phasebook",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A theme for Meith with a familiar social shape, 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-
|
|
24
|
-
"@meith/
|
|
25
|
-
"@meith/
|
|
23
|
+
"@meith/theme-kit": "^0.4.0",
|
|
24
|
+
"@meith/theme-default": "^0.4.0",
|
|
25
|
+
"@meith/ui": "^0.4.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"react": "^19.2.0"
|
package/src/slots/panel-page.tsx
CHANGED
|
@@ -3,14 +3,20 @@ import type { PanelPageModel, PanelSectionModel } from '@meith/theme-kit'
|
|
|
3
3
|
|
|
4
4
|
import { MUTED_LINK } 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
12
|
'flex w-full flex-col',
|
|
13
|
-
|
|
13
|
+
frame === 'standalone'
|
|
14
|
+
? `mx-auto flex-1 px-3 py-4 sm:px-4 lg:py-6 ${
|
|
15
|
+
width === 'wide' ? 'max-w-6xl' : 'max-w-4xl'
|
|
16
|
+
}`
|
|
17
|
+
: width === 'wide'
|
|
18
|
+
? 'max-w-none'
|
|
19
|
+
: 'max-w-4xl',
|
|
14
20
|
gap === 'loose' ? 'gap-4' : 'gap-3',
|
|
15
21
|
)}
|
|
16
22
|
>
|
|
@@ -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 { PILL_PRIMARY } from '../shared'
|
|
4
4
|
|
|
@@ -36,6 +36,62 @@ function Choice({
|
|
|
36
36
|
)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function Advanced({ label, isOpen, author, toggles, choices }: SearchAdvancedModel) {
|
|
40
|
+
return (
|
|
41
|
+
<details open={isOpen} className="rounded-lg border border-border bg-surface/60">
|
|
42
|
+
<summary className="cursor-default px-3 py-2 text-sm font-semibold select-none">
|
|
43
|
+
{label}
|
|
44
|
+
</summary>
|
|
45
|
+
|
|
46
|
+
<div className="flex flex-col gap-4 border-t border-border px-3 py-3">
|
|
47
|
+
<div>
|
|
48
|
+
<label htmlFor="search-author" className={LABEL}>
|
|
49
|
+
{author.label}
|
|
50
|
+
</label>
|
|
51
|
+
<input
|
|
52
|
+
id="search-author"
|
|
53
|
+
type="text"
|
|
54
|
+
name={author.field}
|
|
55
|
+
defaultValue={author.value}
|
|
56
|
+
placeholder={author.placeholder}
|
|
57
|
+
autoComplete="off"
|
|
58
|
+
aria-describedby="search-author-hint"
|
|
59
|
+
className="w-full rounded-full border border-input bg-surface px-4 py-2.5 text-sm text-foreground transition-colors focus-visible:border-ring"
|
|
60
|
+
/>
|
|
61
|
+
<p id="search-author-hint" className="mt-1.5 text-xs text-muted-foreground">
|
|
62
|
+
{author.hint}
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{toggles.map((toggle) => (
|
|
67
|
+
<label key={toggle.field} className="flex items-center gap-2 text-sm select-none">
|
|
68
|
+
<input
|
|
69
|
+
type="checkbox"
|
|
70
|
+
name={toggle.field}
|
|
71
|
+
value={toggle.value}
|
|
72
|
+
defaultChecked={toggle.isOn}
|
|
73
|
+
className="size-4 rounded border border-input accent-primary"
|
|
74
|
+
/>
|
|
75
|
+
{toggle.label}
|
|
76
|
+
</label>
|
|
77
|
+
))}
|
|
78
|
+
|
|
79
|
+
<div className="grid gap-4 sm:grid-cols-3">
|
|
80
|
+
{choices.map((choice) => (
|
|
81
|
+
<Choice
|
|
82
|
+
key={choice.field}
|
|
83
|
+
id={`search-${choice.field}`}
|
|
84
|
+
label={choice.label}
|
|
85
|
+
name={choice.field}
|
|
86
|
+
options={choice.options}
|
|
87
|
+
/>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</details>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
39
95
|
export function SearchForm({
|
|
40
96
|
action,
|
|
41
97
|
fields,
|
|
@@ -45,6 +101,7 @@ export function SearchForm({
|
|
|
45
101
|
sorts,
|
|
46
102
|
hint,
|
|
47
103
|
errorMessage,
|
|
104
|
+
advanced,
|
|
48
105
|
}: SearchFormModel) {
|
|
49
106
|
const described = [hint === null ? null : 'search-hint', errorMessage === null ? null : 'search-error']
|
|
50
107
|
.filter((id): id is string => id !== null)
|
|
@@ -88,6 +145,8 @@ export function SearchForm({
|
|
|
88
145
|
<Choice id="search-sort" label="Sort by" name={fields.sort} options={sorts} />
|
|
89
146
|
</div>
|
|
90
147
|
|
|
148
|
+
{advanced !== undefined && <Advanced {...advanced} />}
|
|
149
|
+
|
|
91
150
|
<div>
|
|
92
151
|
<button type="submit" className={PILL_PRIMARY}>
|
|
93
152
|
Search
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SearchResultsModel } from '@meith/theme-kit'
|
|
1
|
+
import type { OptionModel, SearchRefineModel, SearchResultsModel } from '@meith/theme-kit'
|
|
2
2
|
|
|
3
3
|
import { FEED, LINK, PAGE, PILL, PILL_PRIMARY, Stamp } from '../shared'
|
|
4
4
|
|
|
@@ -10,6 +10,7 @@ export function SearchResults({
|
|
|
10
10
|
nextLabel,
|
|
11
11
|
newSearchHref,
|
|
12
12
|
within,
|
|
13
|
+
refine,
|
|
13
14
|
}: SearchResultsModel) {
|
|
14
15
|
return (
|
|
15
16
|
<main id="board-content" tabIndex={-1} className={`${PAGE} flex-1 py-4 sm:py-6`}>
|
|
@@ -24,6 +25,8 @@ export function SearchResults({
|
|
|
24
25
|
</p>
|
|
25
26
|
</header>
|
|
26
27
|
|
|
28
|
+
{refine !== undefined && <Refine {...refine} />}
|
|
29
|
+
|
|
27
30
|
{hits.length === 0 ? (
|
|
28
31
|
<p className="rounded-lg border border-border bg-card px-4 py-6 text-center text-sm text-muted-foreground shadow-elevation">
|
|
29
32
|
Nothing matched — or nothing you can see does. Try fewer words, or a different
|
|
@@ -63,6 +66,15 @@ export function SearchResults({
|
|
|
63
66
|
action={within.action}
|
|
64
67
|
className="flex flex-col gap-3 px-4 py-4"
|
|
65
68
|
>
|
|
69
|
+
{(within.hidden ?? []).map((field) => (
|
|
70
|
+
<input
|
|
71
|
+
key={`${field.name}-${field.value}`}
|
|
72
|
+
type="hidden"
|
|
73
|
+
name={field.name}
|
|
74
|
+
value={field.value}
|
|
75
|
+
/>
|
|
76
|
+
))}
|
|
77
|
+
|
|
66
78
|
<div>
|
|
67
79
|
<label
|
|
68
80
|
htmlFor="search-within"
|
|
@@ -99,3 +111,114 @@ export function SearchResults({
|
|
|
99
111
|
</main>
|
|
100
112
|
)
|
|
101
113
|
}
|
|
114
|
+
|
|
115
|
+
function Refine({
|
|
116
|
+
action,
|
|
117
|
+
label,
|
|
118
|
+
summary,
|
|
119
|
+
note,
|
|
120
|
+
sorts,
|
|
121
|
+
sortsLabel,
|
|
122
|
+
choices,
|
|
123
|
+
submitLabel,
|
|
124
|
+
applied,
|
|
125
|
+
clearHref,
|
|
126
|
+
}: SearchRefineModel) {
|
|
127
|
+
return (
|
|
128
|
+
<section
|
|
129
|
+
aria-label={label}
|
|
130
|
+
className="flex flex-col gap-2.5 rounded-lg border border-border bg-card px-4 py-3 shadow-elevation"
|
|
131
|
+
>
|
|
132
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
|
|
133
|
+
<p className="text-sm font-semibold text-foreground">{summary}</p>
|
|
134
|
+
|
|
135
|
+
<nav
|
|
136
|
+
aria-label={sortsLabel}
|
|
137
|
+
className="flex items-center rounded-full border border-border p-0.5"
|
|
138
|
+
>
|
|
139
|
+
{sorts.map((sort) => (
|
|
140
|
+
<a
|
|
141
|
+
key={sort.label}
|
|
142
|
+
href={sort.href}
|
|
143
|
+
{...(sort.isCurrent ? { 'aria-current': 'true' as const } : {})}
|
|
144
|
+
className={
|
|
145
|
+
sort.isCurrent
|
|
146
|
+
? 'rounded-full bg-primary/10 px-3 py-1 text-xs font-semibold text-primary'
|
|
147
|
+
: 'rounded-full px-3 py-1 text-xs font-semibold text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
148
|
+
}
|
|
149
|
+
>
|
|
150
|
+
{sort.label}
|
|
151
|
+
</a>
|
|
152
|
+
))}
|
|
153
|
+
</nav>
|
|
154
|
+
|
|
155
|
+
{applied.map((chip) => (
|
|
156
|
+
<a
|
|
157
|
+
key={chip.label}
|
|
158
|
+
href={chip.removeHref}
|
|
159
|
+
className="inline-flex items-center gap-1.5 rounded-full border border-primary/40 bg-primary/10 px-3 py-1 text-xs font-semibold text-foreground hover:bg-primary/20"
|
|
160
|
+
>
|
|
161
|
+
{chip.label}
|
|
162
|
+
<span aria-hidden="true">×</span>
|
|
163
|
+
<span className="sr-only">— remove this filter</span>
|
|
164
|
+
</a>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<form
|
|
169
|
+
method="get"
|
|
170
|
+
action={action}
|
|
171
|
+
className="flex flex-wrap items-center gap-x-3 gap-y-2 border-t border-border pt-2.5"
|
|
172
|
+
>
|
|
173
|
+
{choices.map((choice) => (
|
|
174
|
+
<RefineChoice key={choice.field} {...choice} />
|
|
175
|
+
))}
|
|
176
|
+
|
|
177
|
+
<button type="submit" className={`${PILL} h-8 px-3 text-xs`}>
|
|
178
|
+
{submitLabel}
|
|
179
|
+
</button>
|
|
180
|
+
|
|
181
|
+
{clearHref !== null && (
|
|
182
|
+
<a href={clearHref} className={`text-xs font-semibold ${LINK}`}>
|
|
183
|
+
Clear filters
|
|
184
|
+
</a>
|
|
185
|
+
)}
|
|
186
|
+
</form>
|
|
187
|
+
|
|
188
|
+
{note !== null && <p className="text-xs text-muted-foreground">{note}</p>}
|
|
189
|
+
</section>
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function RefineChoice({
|
|
194
|
+
field,
|
|
195
|
+
label,
|
|
196
|
+
options,
|
|
197
|
+
}: {
|
|
198
|
+
field: string
|
|
199
|
+
label: string
|
|
200
|
+
options: readonly OptionModel[]
|
|
201
|
+
}) {
|
|
202
|
+
const selected = options.find((option) => option.isSelected)
|
|
203
|
+
const id = `search-refine-${field}`
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<span className="inline-flex items-center gap-1.5">
|
|
207
|
+
<label htmlFor={id} className="text-xs font-semibold text-muted-foreground">
|
|
208
|
+
{label}
|
|
209
|
+
</label>
|
|
210
|
+
<select
|
|
211
|
+
id={id}
|
|
212
|
+
name={field}
|
|
213
|
+
defaultValue={selected?.value ?? ''}
|
|
214
|
+
className="h-8 rounded-lg border border-input bg-surface px-2 text-xs text-foreground transition-colors focus-visible:border-ring"
|
|
215
|
+
>
|
|
216
|
+
{options.map((option) => (
|
|
217
|
+
<option key={option.value} value={option.value}>
|
|
218
|
+
{option.label}
|
|
219
|
+
</option>
|
|
220
|
+
))}
|
|
221
|
+
</select>
|
|
222
|
+
</span>
|
|
223
|
+
)
|
|
224
|
+
}
|
package/src/slots/user-panel.tsx
CHANGED
|
@@ -79,6 +79,8 @@ export function UserPanel({
|
|
|
79
79
|
links,
|
|
80
80
|
unreadNotifications,
|
|
81
81
|
unreadMessages,
|
|
82
|
+
notificationsHref,
|
|
83
|
+
messagesHref,
|
|
82
84
|
children,
|
|
83
85
|
}: UserPanelModel) {
|
|
84
86
|
if (viewer.isGuest) {
|
|
@@ -94,8 +96,6 @@ export function UserPanel({
|
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
const name = viewer.username ?? 'Signed in'
|
|
97
|
-
const hrefFor = (label: string): string | null =>
|
|
98
|
-
links.find((link) => link.label === label)?.href ?? null
|
|
99
99
|
|
|
100
100
|
return (
|
|
101
101
|
<div className="flex items-center gap-2">
|
|
@@ -107,14 +107,14 @@ export function UserPanel({
|
|
|
107
107
|
/>
|
|
108
108
|
|
|
109
109
|
{unreadNotifications > 0 && (
|
|
110
|
-
<IconButton href={
|
|
110
|
+
<IconButton href={notificationsHref ?? null} title="Notifications">
|
|
111
111
|
<BellIcon />
|
|
112
112
|
<CountBadge value={unreadNotifications} label="unread notifications" />
|
|
113
113
|
</IconButton>
|
|
114
114
|
)}
|
|
115
115
|
|
|
116
116
|
{unreadMessages > 0 && (
|
|
117
|
-
<IconButton href={
|
|
117
|
+
<IconButton href={messagesHref ?? null} title="Messages">
|
|
118
118
|
<MessageIcon />
|
|
119
119
|
<CountBadge value={unreadMessages} label="unread messages" />
|
|
120
120
|
</IconButton>
|