@webx-ui/module-faq 0.1.14 → 0.1.15
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/index.js +68 -67
- package/dist/index.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -79,7 +79,7 @@ const Xe = {
|
|
|
79
79
|
saved: "The category is saved."
|
|
80
80
|
}
|
|
81
81
|
}, ue = /* @__PURE__ */ new WeakSet();
|
|
82
|
-
function
|
|
82
|
+
function oe() {
|
|
83
83
|
try {
|
|
84
84
|
const { i18n: l } = ee();
|
|
85
85
|
ue.has(l) || (l.defaults("webx-faq", Xe), ue.add(l));
|
|
@@ -91,9 +91,9 @@ const Ye = { class: "wx-faq-anchor" }, Ze = /* @__PURE__ */ ae({
|
|
|
91
91
|
__name: "AnchorField",
|
|
92
92
|
setup(l) {
|
|
93
93
|
const r = Ge();
|
|
94
|
-
|
|
94
|
+
oe();
|
|
95
95
|
const s = te("webx-faq"), a = q(() => r?.question.value?.anchor ?? ""), c = q(() => a.value === "" ? "" : `#${a.value}`);
|
|
96
|
-
async function
|
|
96
|
+
async function o() {
|
|
97
97
|
try {
|
|
98
98
|
await navigator.clipboard.writeText(c.value), w.success(s("question.link-copied"));
|
|
99
99
|
} catch {
|
|
@@ -114,7 +114,7 @@ const Ye = { class: "wx-faq-anchor" }, Ze = /* @__PURE__ */ ae({
|
|
|
114
114
|
y(t(ve), {
|
|
115
115
|
variant: "outline",
|
|
116
116
|
size: "sm",
|
|
117
|
-
onClick:
|
|
117
|
+
onClick: o
|
|
118
118
|
}, {
|
|
119
119
|
icon: d(() => [
|
|
120
120
|
y(t(X), { name: "copy" })
|
|
@@ -136,12 +136,12 @@ const Ye = { class: "wx-faq-anchor" }, Ze = /* @__PURE__ */ ae({
|
|
|
136
136
|
}))
|
|
137
137
|
]));
|
|
138
138
|
}
|
|
139
|
-
}),
|
|
139
|
+
}), ne = (l, r) => {
|
|
140
140
|
const s = l.__vccOpts || l;
|
|
141
141
|
for (const [a, c] of r)
|
|
142
142
|
s[a] = c;
|
|
143
143
|
return s;
|
|
144
|
-
}, et = /* @__PURE__ */
|
|
144
|
+
}, et = /* @__PURE__ */ ne(Ze, [["__scopeId", "data-v-e6091b48"]]), Z = "faq/questions";
|
|
145
145
|
function he(l) {
|
|
146
146
|
const r = `${l.apiPath}/${Z}`, s = (a) => a.data;
|
|
147
147
|
return {
|
|
@@ -168,32 +168,32 @@ const tt = /* @__PURE__ */ ae({
|
|
|
168
168
|
},
|
|
169
169
|
emits: ["back", "saved", "created", "removed"],
|
|
170
170
|
setup(l, { emit: r }) {
|
|
171
|
-
const s = l, a = r, c = ee(),
|
|
172
|
-
|
|
171
|
+
const s = l, a = r, c = ee(), o = he(c), B = Ve();
|
|
172
|
+
oe();
|
|
173
173
|
const u = te("webx-faq"), Q = ce(), x = S(null), g = S({}), N = S(""), C = S(s.id !== null), f = S(!1), T = S({}), k = q(() => c.can("faq.manage")), V = q(() => JSON.stringify(g.value)), m = q(() => N.value !== "" && V.value !== N.value), R = q(() => f.value ? "saving" : m.value || s.id === null ? "unsaved" : "saved"), J = q(() => {
|
|
174
|
-
const
|
|
175
|
-
return Fe(
|
|
174
|
+
const n = g.value.question, h = s.id === null ? u("question.new") : x.value?.question ?? "";
|
|
175
|
+
return Fe(n, B.active.value, h) || u("question.untitled");
|
|
176
176
|
});
|
|
177
177
|
je({ question: x });
|
|
178
|
-
function F(
|
|
179
|
-
x.value =
|
|
178
|
+
function F(n) {
|
|
179
|
+
x.value = n.question, g.value = n.values, N.value = JSON.stringify(n.values);
|
|
180
180
|
}
|
|
181
181
|
if (s.id === null) {
|
|
182
|
-
const
|
|
182
|
+
const n = {
|
|
183
183
|
question: {},
|
|
184
184
|
answer: {},
|
|
185
185
|
published: !1,
|
|
186
186
|
categories: s.category === null ? [] : [s.category]
|
|
187
187
|
};
|
|
188
|
-
g.value =
|
|
188
|
+
g.value = n, N.value = JSON.stringify(n);
|
|
189
189
|
}
|
|
190
190
|
async function U() {
|
|
191
191
|
if (s.id !== null) {
|
|
192
192
|
C.value = !0;
|
|
193
193
|
try {
|
|
194
|
-
F(await
|
|
195
|
-
} catch (
|
|
196
|
-
w.danger(Q(
|
|
194
|
+
F(await o.get(s.id));
|
|
195
|
+
} catch (n) {
|
|
196
|
+
w.danger(Q(n)), a("back");
|
|
197
197
|
} finally {
|
|
198
198
|
C.value = !1;
|
|
199
199
|
}
|
|
@@ -203,36 +203,36 @@ const tt = /* @__PURE__ */ ae({
|
|
|
203
203
|
if (!(f.value || !k.value)) {
|
|
204
204
|
f.value = !0, T.value = {};
|
|
205
205
|
try {
|
|
206
|
-
const
|
|
207
|
-
F(h), w.success(u("question.saved")), a(
|
|
208
|
-
} catch (
|
|
209
|
-
const h =
|
|
210
|
-
h?.errors ? (T.value = h.errors, w.danger(u("question.save-failed"))) : w.danger(Q(
|
|
206
|
+
const n = s.id === null, h = n ? await o.create(g.value) : await o.save(s.id, g.value);
|
|
207
|
+
F(h), w.success(u("question.saved")), a(n ? "created" : "saved", h.question);
|
|
208
|
+
} catch (n) {
|
|
209
|
+
const h = n.body;
|
|
210
|
+
h?.errors ? (T.value = h.errors, w.danger(u("question.save-failed"))) : w.danger(Q(n));
|
|
211
211
|
} finally {
|
|
212
212
|
f.value = !1;
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
async function W() {
|
|
217
|
-
const
|
|
218
|
-
if (!(!
|
|
219
|
-
title: u("question.delete-title", { title:
|
|
217
|
+
const n = x.value;
|
|
218
|
+
if (!(!n || !await Y({
|
|
219
|
+
title: u("question.delete-title", { title: n.question }),
|
|
220
220
|
message: u("question.delete-text"),
|
|
221
221
|
confirmText: u("question.delete"),
|
|
222
222
|
cancelText: u("question.cancel"),
|
|
223
223
|
tone: "danger"
|
|
224
224
|
})))
|
|
225
225
|
try {
|
|
226
|
-
await
|
|
226
|
+
await o.remove(n.id), w.success(u("question.deleted")), N.value = V.value, a("removed", n.id);
|
|
227
227
|
} catch (P) {
|
|
228
228
|
w.danger(Q(P));
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
|
-
function z(
|
|
232
|
-
m.value &&
|
|
231
|
+
function z(n) {
|
|
232
|
+
m.value && n.preventDefault();
|
|
233
233
|
}
|
|
234
|
-
function D(
|
|
235
|
-
(
|
|
234
|
+
function D(n) {
|
|
235
|
+
(n.ctrlKey || n.metaKey) && n.key.toLowerCase() === "s" && (n.preventDefault(), (m.value || s.id === null) && E());
|
|
236
236
|
}
|
|
237
237
|
be(D), Be(() => {
|
|
238
238
|
U(), window.addEventListener("beforeunload", z);
|
|
@@ -249,7 +249,7 @@ const tt = /* @__PURE__ */ ae({
|
|
|
249
249
|
});
|
|
250
250
|
}
|
|
251
251
|
Oe(
|
|
252
|
-
async (
|
|
252
|
+
async (n, h) => n.query.question === h.query.question ? !0 : await I()
|
|
253
253
|
), Ke(I);
|
|
254
254
|
const O = q(
|
|
255
255
|
() => k.value && x.value !== null ? [
|
|
@@ -265,7 +265,7 @@ const tt = /* @__PURE__ */ ae({
|
|
|
265
265
|
}
|
|
266
266
|
] : []
|
|
267
267
|
);
|
|
268
|
-
return (
|
|
268
|
+
return (n, h) => (v(), A("div", {
|
|
269
269
|
class: "wx-faq-question",
|
|
270
270
|
onKeydown: D
|
|
271
271
|
}, [
|
|
@@ -326,7 +326,7 @@ const tt = /* @__PURE__ */ ae({
|
|
|
326
326
|
], 64))
|
|
327
327
|
], 32));
|
|
328
328
|
}
|
|
329
|
-
}), at = /* @__PURE__ */
|
|
329
|
+
}), at = /* @__PURE__ */ ne(tt, [["__scopeId", "data-v-fe493d3f"]]), ot = { class: "wx-faq__bar" }, nt = { class: "wx-faq-row__question" }, st = { class: "wx-faq-row__facts" }, it = {
|
|
330
330
|
key: 2,
|
|
331
331
|
class: "wx-faq-row__locales"
|
|
332
332
|
}, lt = /* @__PURE__ */ ae({
|
|
@@ -336,9 +336,9 @@ const tt = /* @__PURE__ */ ae({
|
|
|
336
336
|
},
|
|
337
337
|
setup(l) {
|
|
338
338
|
const r = ee(), s = he(r), a = He(), c = Je();
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
() => r.state.manifest?.modules.find((e) => e.id === "faq")?.title ??
|
|
339
|
+
oe();
|
|
340
|
+
const o = te("webx-faq"), B = ce(), u = S([]), Q = S([]), x = S(!0), g = q(() => r.can("faq.manage")), N = q(
|
|
341
|
+
() => r.state.manifest?.modules.find((e) => e.id === "faq")?.title ?? o("module.questions")
|
|
342
342
|
), C = q({
|
|
343
343
|
get: () => a.query.view === "trashed" ? "trashed" : "",
|
|
344
344
|
set: (e) => {
|
|
@@ -360,13 +360,13 @@ const tt = /* @__PURE__ */ ae({
|
|
|
360
360
|
e || O();
|
|
361
361
|
}
|
|
362
362
|
}), J = q(() => [
|
|
363
|
-
{ value: "", label:
|
|
364
|
-
{ value: "trashed", label:
|
|
363
|
+
{ value: "", label: o("question.all") },
|
|
364
|
+
{ value: "trashed", label: o("question.bin"), icon: "trash" }
|
|
365
365
|
]), F = Se(Z, () => ({
|
|
366
366
|
q: k.value,
|
|
367
367
|
category: T.value,
|
|
368
368
|
filtered: f.value
|
|
369
|
-
})), U = q(() => g.value && F.sortable.value), E = q(() => k.value !== "" ?
|
|
369
|
+
})), U = q(() => g.value && F.sortable.value), E = q(() => k.value !== "" ? o("question.empty-search") : f.value ? o("question.empty-bin") : o("question.empty"));
|
|
370
370
|
async function W() {
|
|
371
371
|
x.value = !0;
|
|
372
372
|
try {
|
|
@@ -406,14 +406,14 @@ const tt = /* @__PURE__ */ ae({
|
|
|
406
406
|
const e = { ...a.query };
|
|
407
407
|
delete e.question, c.replace({ query: e });
|
|
408
408
|
}
|
|
409
|
-
function
|
|
409
|
+
function n(e) {
|
|
410
410
|
I(e.id), W();
|
|
411
411
|
}
|
|
412
412
|
async function h() {
|
|
413
413
|
try {
|
|
414
414
|
await F.move(u.value.map((e) => e.id));
|
|
415
415
|
} catch (e) {
|
|
416
|
-
w.danger(B(e,
|
|
416
|
+
w.danger(B(e, o("question.reorder-failed"))), await W();
|
|
417
417
|
}
|
|
418
418
|
}
|
|
419
419
|
function P(e) {
|
|
@@ -421,7 +421,7 @@ const tt = /* @__PURE__ */ ae({
|
|
|
421
421
|
{
|
|
422
422
|
key: "restore",
|
|
423
423
|
icon: "refresh",
|
|
424
|
-
label:
|
|
424
|
+
label: o("question.restore"),
|
|
425
425
|
run: () => {
|
|
426
426
|
pe(e);
|
|
427
427
|
}
|
|
@@ -430,7 +430,7 @@ const tt = /* @__PURE__ */ ae({
|
|
|
430
430
|
{
|
|
431
431
|
key: "copy",
|
|
432
432
|
icon: "copy",
|
|
433
|
-
label:
|
|
433
|
+
label: o("question.copy-link"),
|
|
434
434
|
run: () => {
|
|
435
435
|
ge(e);
|
|
436
436
|
}
|
|
@@ -438,7 +438,7 @@ const tt = /* @__PURE__ */ ae({
|
|
|
438
438
|
{
|
|
439
439
|
key: "delete",
|
|
440
440
|
icon: "trash",
|
|
441
|
-
label:
|
|
441
|
+
label: o("question.delete"),
|
|
442
442
|
danger: !0,
|
|
443
443
|
run: () => {
|
|
444
444
|
ye(e);
|
|
@@ -448,14 +448,14 @@ const tt = /* @__PURE__ */ ae({
|
|
|
448
448
|
}
|
|
449
449
|
async function ye(e) {
|
|
450
450
|
if (await Y({
|
|
451
|
-
title:
|
|
452
|
-
message:
|
|
453
|
-
confirmText:
|
|
454
|
-
cancelText:
|
|
451
|
+
title: o("question.delete-title", { title: e.question }),
|
|
452
|
+
message: o("question.delete-text"),
|
|
453
|
+
confirmText: o("question.delete"),
|
|
454
|
+
cancelText: o("question.cancel"),
|
|
455
455
|
tone: "danger"
|
|
456
456
|
}))
|
|
457
457
|
try {
|
|
458
|
-
await s.remove(e.id), w.success(
|
|
458
|
+
await s.remove(e.id), w.success(o("question.deleted")), se(e.id);
|
|
459
459
|
} catch (i) {
|
|
460
460
|
w.danger(B(i));
|
|
461
461
|
}
|
|
@@ -465,7 +465,7 @@ const tt = /* @__PURE__ */ ae({
|
|
|
465
465
|
}
|
|
466
466
|
async function pe(e) {
|
|
467
467
|
try {
|
|
468
|
-
await s.restore(e.id), w.success(
|
|
468
|
+
await s.restore(e.id), w.success(o("question.restored"));
|
|
469
469
|
} catch (p) {
|
|
470
470
|
w.danger(B(p));
|
|
471
471
|
} finally {
|
|
@@ -474,7 +474,7 @@ const tt = /* @__PURE__ */ ae({
|
|
|
474
474
|
}
|
|
475
475
|
async function ge(e) {
|
|
476
476
|
try {
|
|
477
|
-
await navigator.clipboard.writeText(`#${e.anchor}`), w.success(
|
|
477
|
+
await navigator.clipboard.writeText(`#${e.anchor}`), w.success(o("question.link-copied"));
|
|
478
478
|
} catch {
|
|
479
479
|
}
|
|
480
480
|
}
|
|
@@ -500,18 +500,18 @@ const tt = /* @__PURE__ */ ae({
|
|
|
500
500
|
class: "wx-faq__panes",
|
|
501
501
|
"list-width": 360,
|
|
502
502
|
"detail-min": 460,
|
|
503
|
-
"detail-label": t(
|
|
503
|
+
"detail-label": t(o)("module.questions")
|
|
504
504
|
}, {
|
|
505
505
|
list: d(() => [
|
|
506
|
-
j("div",
|
|
506
|
+
j("div", ot, [
|
|
507
507
|
y(t(Me), {
|
|
508
508
|
modelValue: V.value,
|
|
509
509
|
"onUpdate:modelValue": p[0] || (p[0] = (i) => V.value = i),
|
|
510
510
|
class: "wx-faq__search",
|
|
511
511
|
size: "sm",
|
|
512
512
|
clearable: "",
|
|
513
|
-
placeholder: t(
|
|
514
|
-
"aria-label": t(
|
|
513
|
+
placeholder: t(o)("question.search"),
|
|
514
|
+
"aria-label": t(o)("question.search")
|
|
515
515
|
}, {
|
|
516
516
|
prefix: d(() => [
|
|
517
517
|
y(t(X), {
|
|
@@ -525,8 +525,8 @@ const tt = /* @__PURE__ */ ae({
|
|
|
525
525
|
key: 0,
|
|
526
526
|
"model-value": T.value,
|
|
527
527
|
options: Q.value.map((i) => ({ value: i.id, label: i.title })),
|
|
528
|
-
placeholder: t(
|
|
529
|
-
"aria-label": t(
|
|
528
|
+
placeholder: t(o)("question.any-category"),
|
|
529
|
+
"aria-label": t(o)("question.filter-category"),
|
|
530
530
|
clearable: "",
|
|
531
531
|
size: "sm",
|
|
532
532
|
"onUpdate:modelValue": D
|
|
@@ -542,7 +542,7 @@ const tt = /* @__PURE__ */ ae({
|
|
|
542
542
|
name: "plus",
|
|
543
543
|
size: "sm"
|
|
544
544
|
}),
|
|
545
|
-
$(" " + _(t(
|
|
545
|
+
$(" " + _(t(o)("question.new")), 1)
|
|
546
546
|
], 2)) : L("", !0),
|
|
547
547
|
x.value && u.value.length === 0 ? (v(), b(t(fe), {
|
|
548
548
|
key: 1,
|
|
@@ -551,12 +551,13 @@ const tt = /* @__PURE__ */ ae({
|
|
|
551
551
|
})) : u.value.length === 0 ? (v(), b(t(re), {
|
|
552
552
|
key: 2,
|
|
553
553
|
title: E.value,
|
|
554
|
-
description: !f.value && k.value === "" ? t(
|
|
554
|
+
description: !f.value && k.value === "" ? t(o)("question.empty-help") : void 0
|
|
555
555
|
}, null, 8, ["title", "description"])) : (v(), A(H, { key: 3 }, [
|
|
556
556
|
y(t(Ue), {
|
|
557
557
|
modelValue: u.value,
|
|
558
558
|
"onUpdate:modelValue": p[2] || (p[2] = (i) => u.value = i),
|
|
559
559
|
class: G(["wx-faq__list", { "is-loading": x.value }]),
|
|
560
|
+
hoverable: "",
|
|
560
561
|
plain: "",
|
|
561
562
|
"item-key": "id",
|
|
562
563
|
handle: U.value ? "grip" : "row",
|
|
@@ -571,11 +572,11 @@ const tt = /* @__PURE__ */ ae({
|
|
|
571
572
|
onClick: (M) => f.value ? void 0 : I(i.id)
|
|
572
573
|
}, {
|
|
573
574
|
default: d(() => [
|
|
574
|
-
j("span",
|
|
575
|
+
j("span", nt, _(i.question), 1),
|
|
575
576
|
j("span", st, [
|
|
576
577
|
i.published ? me(i) ? (v(), b(t(Ee), {
|
|
577
578
|
key: 1,
|
|
578
|
-
content: t(
|
|
579
|
+
content: t(o)("question.seen-nowhere")
|
|
579
580
|
}, {
|
|
580
581
|
default: d(() => [
|
|
581
582
|
y(t(K), {
|
|
@@ -584,7 +585,7 @@ const tt = /* @__PURE__ */ ae({
|
|
|
584
585
|
size: "sm"
|
|
585
586
|
}, {
|
|
586
587
|
default: d(() => [
|
|
587
|
-
$(_(t(
|
|
588
|
+
$(_(t(o)("question.not-published")), 1)
|
|
588
589
|
]),
|
|
589
590
|
_: 1
|
|
590
591
|
})
|
|
@@ -596,7 +597,7 @@ const tt = /* @__PURE__ */ ae({
|
|
|
596
597
|
size: "sm"
|
|
597
598
|
}, {
|
|
598
599
|
default: d(() => [
|
|
599
|
-
$(_(t(
|
|
600
|
+
$(_(t(o)("question.not-published")), 1)
|
|
600
601
|
]),
|
|
601
602
|
_: 1
|
|
602
603
|
})),
|
|
@@ -640,14 +641,14 @@ const tt = /* @__PURE__ */ ae({
|
|
|
640
641
|
inline: i,
|
|
641
642
|
onBack: M,
|
|
642
643
|
onSaved: W,
|
|
643
|
-
onCreated:
|
|
644
|
+
onCreated: n,
|
|
644
645
|
onRemoved: se
|
|
645
646
|
}, null, 8, ["id", "category", "inline", "onBack"])) : L("", !0)
|
|
646
647
|
]),
|
|
647
648
|
empty: d(() => [
|
|
648
649
|
y(t(re), {
|
|
649
|
-
title: t(
|
|
650
|
-
description: g.value ? t(
|
|
650
|
+
title: t(o)("question.choose"),
|
|
651
|
+
description: g.value ? t(o)("question.choose-help") : void 0
|
|
651
652
|
}, null, 8, ["title", "description"])
|
|
652
653
|
]),
|
|
653
654
|
_: 1
|
|
@@ -659,7 +660,7 @@ const tt = /* @__PURE__ */ ae({
|
|
|
659
660
|
_: 1
|
|
660
661
|
}, 8, ["view", "title", "views"]));
|
|
661
662
|
}
|
|
662
|
-
}), rt = /* @__PURE__ */
|
|
663
|
+
}), rt = /* @__PURE__ */ ne(lt, [["__scopeId", "data-v-adc3e0e7"]]);
|
|
663
664
|
function ut(l = "/faq") {
|
|
664
665
|
return {
|
|
665
666
|
api: "faq/categories",
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/editor.ts","../src/messages.ts","../src/i18n.ts","../src/AnchorField.vue","../src/api.ts","../src/QuestionPane.vue","../src/QuestionsPage.vue","../src/module.ts"],"sourcesContent":["import { inject, provide, type InjectionKey, type Ref } from 'vue'\nimport type { QuestionRow } from './types'\n\n/**\n * What the form knows and the nodes of `faq.form` do not: the question being edited.\n *\n * The anchor is not a value of the screen — nobody writes it, it is made once on the server and\n * never changes (decision 10) — so the node that shows it (`wx-faq-anchor`) reads the record.\n */\nexport interface QuestionEditorContext {\n /** The question as the server last answered it; `null` while it is new and not yet saved. */\n question: Ref<QuestionRow | null>\n}\n\nexport const questionEditorKey: InjectionKey<QuestionEditorContext> = Symbol('wx-faq-question')\n\nexport function provideQuestionEditor(editor: QuestionEditorContext): void {\n provide(questionEditorKey, editor)\n}\n\n/** The form above this node, or `null` outside one — a demo or a test draws the empty state. */\nexport function useQuestionEditor(): QuestionEditorContext | null {\n return inject(questionEditorKey, null)\n}\n","import type { Messages } from '@webx-ui/module-admin'\n\n/**\n * The English this package speaks on its own, before the server's dictionary arrives, or without\n * one. The server ships the same lines in ten languages under `webx-faq::` and overrides these;\n * `messages.test.ts` keeps the two sets of keys equal.\n */\nexport const faqMessages: Record<string, Messages> = {\n module: {\n group: 'FAQ',\n questions: 'Questions',\n categories: 'Categories',\n },\n question: {\n new: 'New question',\n untitled: 'Untitled',\n search: 'Search the questions',\n empty: 'No questions yet.',\n 'empty-help': 'A question reaches the site in a FAQ block, on any page.',\n 'empty-search': 'Nothing matches that.',\n 'empty-bin': 'The bin is empty.',\n all: 'All',\n bin: 'Bin',\n 'filter-category': 'Category',\n 'any-category': 'Any category',\n 'not-published': 'Not published',\n 'seen-nowhere': 'Not seen in any language: it needs a question and an answer in one.',\n choose: 'Choose a question',\n 'choose-help': 'Or write a new one: the first line of the list.',\n save: 'Save',\n saved: 'The question is saved.',\n 'save-failed': 'The question was not saved.',\n cancel: 'Cancel',\n delete: 'Delete',\n 'delete-title': 'Delete “:title”?',\n 'delete-text':\n 'It goes to the bin and leaves every block on the site. Restored, it comes back with the same link.',\n deleted: 'The question is in the bin.',\n restore: 'Restore',\n restored: 'The question is back.',\n 'copy-link': 'Copy the link',\n 'link-copied': 'The link is on the clipboard.',\n 'anchor-later': 'Made from the question when it is first saved.',\n 'reorder-failed': 'The new order was not saved.',\n 'leave-title': 'Leave without saving?',\n 'leave-text': 'What you wrote in this question is not on the server.',\n leave: 'Leave',\n back: 'Back to the list',\n },\n screen: {\n question: 'Question',\n answer: 'Answer',\n 'answer-help':\n 'A question is shown in a language only when it has both the question and the answer in it.',\n settings: 'Settings',\n published: 'Published',\n 'published-help':\n 'On the site in every block that shows its categories, from the moment it is saved.',\n categories: 'Categories',\n 'categories-help': 'What a block picks the question by, and the buttons of its filter.',\n anchor: 'Link',\n 'anchor-help': 'Made once from the question and never changed, so links to it keep working.',\n },\n category: {\n 'field-title': 'Title',\n visible: 'Shown on the site',\n 'visible-help':\n 'Hidden, it drops out of every filter; its questions stay in the blocks that show them.',\n new: 'New category',\n empty: 'No categories yet.',\n 'empty-help':\n 'A category groups questions: a block shows the ones you pick, and its filter has a button for each.',\n order: 'Drag to change the order of the filter buttons on the site.',\n hidden: 'Hidden from the site',\n questions: 'Questions: :count',\n 'show-questions': 'Show its questions',\n 'delete-blocked': 'This category still holds questions. Move them first.',\n 'delete-text': 'It goes to the bin and its button leaves every filter. Its questions stay.',\n deleted: 'The category is in the bin.',\n saved: 'The category is saved.',\n },\n}\n","import { useAdmin } from '@webx-ui/module-admin'\nimport { faqMessages } from './messages'\n\nconst seeded = new WeakSet<object>()\n\n/** Puts this package's English under the panel's dictionary, once per panel. */\nexport function useFaqMessages(): void {\n try {\n const { i18n } = useAdmin()\n\n if (!seeded.has(i18n)) {\n i18n.defaults('webx-faq', faqMessages)\n seeded.add(i18n)\n }\n } catch {\n // Outside a panel there is no dictionary to seed, and `useTranslate` falls back on its own.\n }\n}\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { toast, WxButton, WxIcon, WxText } from '@webx-ui/core'\nimport { useTranslate } from '@webx-ui/module-admin'\nimport { useQuestionEditor } from './editor'\nimport { useFaqMessages } from './i18n'\n\n/**\n * The anchor of a question: shown, never written (decision 10).\n *\n * Registered as a field only so that the screen draws it the way it draws a field — the label and\n * the help of its node around it. It has no name, so there is no value to bind, and it reads the\n * anchor off the question the form is editing.\n *\n * What is copied is the fragment, `#anchor`, and not an address: a question has no page of its\n * own, and the page it is linked on is whichever page the editor put the block on.\n */\ndefineOptions({ name: 'WxFaqAnchor' })\n\nconst editor = useQuestionEditor()\nuseFaqMessages()\n\nconst t = useTranslate('webx-faq')\n\nconst anchor = computed(() => editor?.question.value?.anchor ?? '')\nconst fragment = computed(() => (anchor.value === '' ? '' : `#${anchor.value}`))\n\nasync function copy(): Promise<void> {\n try {\n await navigator.clipboard.writeText(fragment.value)\n toast.success(t('question.link-copied'))\n } catch {\n // A browser that refuses the clipboard without a gesture it trusts; the anchor is on screen.\n }\n}\n</script>\n\n<template>\n <div class=\"wx-faq-anchor\">\n <template v-if=\"fragment !== ''\">\n <wx-text class=\"wx-faq-anchor__value\" mono truncate>{{ fragment }}</wx-text>\n <wx-button variant=\"outline\" size=\"sm\" @click=\"copy\">\n <template #icon><wx-icon name=\"copy\" /></template>\n {{ t('question.copy-link') }}\n </wx-button>\n </template>\n\n <wx-text v-else size=\"sm\" tone=\"muted\">{{ t('question.anchor-later') }}</wx-text>\n </div>\n</template>\n\n<style scoped>\n.wx-faq-anchor {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--wx-space-8);\n min-width: 0;\n}\n\n.wx-faq-anchor__value {\n flex: 1 1 auto;\n min-width: 0;\n}\n</style>\n","import { reorderItems, type AdminContext } from '@webx-ui/module-admin'\nimport type { ScreenModel } from '@webx-ui/schema'\nimport type { QuestionDetail, QuestionQuery, QuestionRow, QuestionsList } from './types'\n\nexport interface FaqApi {\n /** Every question, with the categories the list can be narrowed to. */\n list(query?: QuestionQuery): Promise<QuestionsList>\n get(id: number): Promise<QuestionDetail>\n /**\n * A new question out of the values of its form, in one request: the form is filled in first\n * and the record made on the first save, so nobody leaves an empty question behind.\n */\n create(values: ScreenModel): Promise<QuestionDetail>\n /** Refused with a 422 under the name of the field. */\n save(id: number, values: ScreenModel): Promise<QuestionDetail>\n remove(id: number): Promise<void>\n restore(id: number): Promise<QuestionRow>\n /** The order on screen: of the whole list, or — with `category` — of that category alone. */\n reorder(ids: number[], category?: number | null): Promise<void>\n}\n\n/** The path of the list under the panel's API, and of its order (`CategoryRoutes::items()`). */\nexport const FAQ_API = 'faq/questions'\n\n/**\n * Everything under `/faq/questions`, below the panel's API path — except the categories, which\n * are the panel's shared ones and are asked for with `createCategoriesApi(admin, 'faq/categories')`.\n */\nexport function createFaqApi(admin: AdminContext): FaqApi {\n const base = `${admin.apiPath}/${FAQ_API}`\n const data = <T>(body: { data: T }): T => body.data\n\n return {\n list: (query = {}) => {\n const search = new URLSearchParams()\n\n // Only what was asked for: `category=` would ask for the questions of no category.\n if (query.search) search.set('search', query.search)\n if (query.category != null) search.set('category', String(query.category))\n if (query.trashed) search.set('trashed', '1')\n\n return admin.http.get<QuestionsList>(search.size > 0 ? `${base}?${search}` : base)\n },\n get: (id) => admin.http.get<{ data: QuestionDetail }>(`${base}/${id}`).then(data),\n create: (values) => admin.http.post<{ data: QuestionDetail }>(base, { values }).then(data),\n save: (id, values) =>\n admin.http.put<{ data: QuestionDetail }>(`${base}/${id}`, { values }).then(data),\n remove: (id) => admin.http.delete<void>(`${base}/${id}`).then(() => undefined),\n restore: (id) => admin.http.post<{ data: QuestionRow }>(`${base}/${id}/restore`, {}).then(data),\n reorder: (ids, category = null) => reorderItems(admin, FAQ_API, ids, category),\n }\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref } from 'vue'\nimport { onBeforeRouteLeave, onBeforeRouteUpdate, type RouteLocationNormalized } from 'vue-router'\nimport {\n confirm,\n localizedValue,\n toast,\n useLocales,\n WxActionBar,\n WxBadge,\n WxButton,\n WxSkeleton,\n type LocalizedValue,\n} from '@webx-ui/core'\nimport {\n useAdmin,\n useErrorText,\n useTranslate,\n WxSaveState,\n WxScreen,\n WxScreenHead,\n type ScreenAction,\n useBodyKeys,\n} from '@webx-ui/module-admin'\nimport type { ScreenModel } from '@webx-ui/schema'\nimport { createFaqApi } from './api'\nimport { provideQuestionEditor } from './editor'\nimport { useFaqMessages } from './i18n'\nimport type { QuestionDetail, QuestionRow } from './types'\n\n/**\n * One question: the pane beside the list, and the whole screen on a phone.\n *\n * The described screen `faq.form` with a head over it and one button under it. One explicit\n * save, and not the autosave of an article: a question has no draft (decision 12), so what is\n * saved is on the site at once — the button is the moment somebody decides it is ready.\n *\n * A new question is a form first and a record on the first save: an empty row made by pressing\n * \"New question\" and walking away would be a question with nothing in it on the list for ever.\n *\n * The way back is drawn here and not by the pane around it: the drawer a narrow screen opens\n * this in has no close of its own on purpose (CLAUDE.md §4).\n */\ndefineOptions({ name: 'WxFaqQuestionPane' })\n\nconst props = withDefaults(\n defineProps<{\n /** `null` — a question not written yet. */\n id: number | null\n /** The category the list is narrowed to: a new question starts in it. */\n category?: number | null\n /** False in the drawer a narrow screen opens this in. */\n inline?: boolean\n }>(),\n { category: null, inline: true },\n)\n\nconst emit = defineEmits<{\n back: []\n /** The question was written; the list redraws its row. */\n saved: [row: QuestionRow]\n /** The first save of a new question: it is a record now, with an id to be opened by. */\n created: [row: QuestionRow]\n removed: [id: number]\n}>()\n\nconst context = useAdmin()\nconst api = createFaqApi(context)\nconst locales = useLocales()\nuseFaqMessages()\n\nconst t = useTranslate('webx-faq')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst question = ref<QuestionRow | null>(null)\nconst values = ref<ScreenModel>({})\nconst snapshot = ref('')\n\nconst loading = ref(props.id !== null)\nconst saving = ref(false)\nconst errors = ref<Record<string, string[]>>({})\n\nconst canManage = computed(() => context.can('faq.manage'))\n\nconst current = computed(() => JSON.stringify(values.value))\nconst dirty = computed(() => snapshot.value !== '' && current.value !== snapshot.value)\n\nconst state = computed<'saving' | 'unsaved' | 'saved'>(() => {\n if (saving.value) return 'saving'\n\n return dirty.value || props.id === null ? 'unsaved' : 'saved'\n})\n\n/** The question follows the field rather than the answer: an edit shows at the top as it is typed. */\nconst title = computed(() => {\n const written = values.value.question as LocalizedValue | string | null | undefined\n const fallback = props.id === null ? t('question.new') : (question.value?.question ?? '')\n\n return localizedValue(written, locales.active.value, fallback) || t('question.untitled')\n})\n\nprovideQuestionEditor({ question })\n\nfunction take(detail: QuestionDetail): void {\n question.value = detail.question\n values.value = detail.values\n snapshot.value = JSON.stringify(detail.values)\n}\n\nif (props.id === null) {\n // What the server would answer for a question with nothing in it yet.\n const blank: ScreenModel = {\n question: {},\n answer: {},\n published: false,\n categories: props.category === null ? [] : [props.category],\n }\n\n values.value = blank\n snapshot.value = JSON.stringify(blank)\n}\n\nasync function load(): Promise<void> {\n if (props.id === null) return\n\n loading.value = true\n\n try {\n take(await api.get(props.id))\n } catch (error) {\n toast.danger(message(error))\n emit('back')\n } finally {\n loading.value = false\n }\n}\n\nasync function save(): Promise<void> {\n if (saving.value || !canManage.value) return\n\n saving.value = true\n errors.value = {}\n\n try {\n const made = props.id === null\n const detail = made\n ? await api.create(values.value)\n : await api.save(props.id as number, values.value)\n\n take(detail)\n toast.success(t('question.saved'))\n if (made) emit('created', detail.question)\n else emit('saved', detail.question)\n } catch (error) {\n const body = (error as { body?: { errors?: Record<string, string[]> } }).body\n\n if (body?.errors) {\n errors.value = body.errors\n toast.danger(t('question.save-failed'))\n } else {\n toast.danger(message(error))\n }\n } finally {\n saving.value = false\n }\n}\n\nasync function remove(): Promise<void> {\n const row = question.value\n\n if (!row) return\n\n const agreed = await confirm({\n title: t('question.delete-title', { title: row.question }),\n message: t('question.delete-text'),\n confirmText: t('question.delete'),\n cancelText: t('question.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.remove(row.id)\n toast.success(t('question.deleted'))\n // Nothing left to protect: the record is in the bin.\n snapshot.value = current.value\n emit('removed', row.id)\n } catch (error) {\n toast.danger(message(error))\n }\n}\n\n/* The browser's own guard: it cannot wait for a request, so all it can do is ask. */\nfunction leaveGuard(event: BeforeUnloadEvent): void {\n if (dirty.value) event.preventDefault()\n}\n\n/* Ctrl+S saves, as it does everywhere else that has a save button. */\nfunction onKeydown(event: KeyboardEvent): void {\n if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {\n event.preventDefault()\n if (dirty.value || props.id === null) void save()\n }\n}\n\n// And from the page around the form, where a click on empty space leaves the focus.\nuseBodyKeys(onKeydown)\n\nonMounted(() => {\n void load()\n window.addEventListener('beforeunload', leaveGuard)\n})\n\nonBeforeUnmount(() => {\n window.removeEventListener('beforeunload', leaveGuard)\n})\n\nasync function mayLeave(): Promise<boolean> {\n if (!dirty.value || !canManage.value) return true\n\n return await confirm({\n title: t('question.leave-title'),\n message: t('question.leave-text'),\n confirmText: t('question.leave'),\n cancelText: t('question.cancel'),\n tone: 'danger',\n })\n}\n\n/*\n * The list and this form are one route, so another question is an update of it rather than a\n * way out — and so is every keystroke in the search above the list, which goes into the address\n * too. Only a change of the question asks.\n */\nonBeforeRouteUpdate(async (to: RouteLocationNormalized, from: RouteLocationNormalized) =>\n to.query.question === from.query.question ? true : await mayLeave(),\n)\n\nonBeforeRouteLeave(mayLeave)\n\n/* What leads away from the question. On a narrow pane the head folds them into the ···. */\nconst actions = computed<ScreenAction[]>(() =>\n canManage.value && question.value !== null\n ? [\n {\n key: 'delete',\n label: t('question.delete'),\n icon: 'trash',\n danger: true,\n menu: true,\n run: () => void remove(),\n },\n ]\n : [],\n)\n</script>\n\n<template>\n <div class=\"wx-faq-question\" @keydown=\"onKeydown\">\n <wx-skeleton v-if=\"loading\" class=\"wx-faq-question__ghost\" title :rows=\"6\" />\n\n <template v-else>\n <wx-screen-head\n :level=\"3\"\n :back=\"!props.inline\"\n :back-label=\"t('question.back')\"\n :title=\"title\"\n :actions=\"actions\"\n :collapse-below=\"480\"\n @back=\"emit('back')\"\n >\n <template v-if=\"question && !question.published\" #title-after>\n <wx-badge dot>{{ t('question.not-published') }}</wx-badge>\n </template>\n </wx-screen-head>\n\n <wx-screen\n v-model=\"values\"\n name=\"faq.form\"\n :errors=\"errors\"\n :disabled=\"!canManage || saving\"\n />\n\n <wx-action-bar v-if=\"canManage\">\n <template #state>\n <wx-save-state :state=\"state\" />\n </template>\n\n <wx-button\n type=\"primary\"\n :loading=\"saving\"\n :disabled=\"!dirty && props.id !== null\"\n @click=\"save\"\n >\n {{ t('question.save') }}\n </wx-button>\n </wx-action-bar>\n </template>\n </div>\n</template>\n\n<style scoped>\n.wx-faq-question {\n display: flex;\n flex-direction: column;\n gap: var(--wx-gap, var(--wx-space-16));\n min-width: 0;\n padding: var(--wx-gap, var(--wx-space-16));\n container-type: inline-size;\n}\n\n.wx-faq-question__ghost {\n max-width: 480px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { useRoute, useRouter, type LocationQueryRaw } from 'vue-router'\nimport {\n confirm,\n toast,\n WxAlert,\n WxBadge,\n WxCard,\n WxEmpty,\n WxIcon,\n WxInput,\n WxListDetail,\n WxSelect,\n WxSkeleton,\n WxSortableList,\n WxTooltip,\n type TabItem,\n type TabValue,\n} from '@webx-ui/core'\nimport {\n useAdmin,\n useErrorText,\n useItemOrder,\n useTranslate,\n WxListScreen,\n WxRowMenu,\n type RowAction,\n} from '@webx-ui/module-admin'\nimport { createFaqApi, FAQ_API } from './api'\nimport { useFaqMessages } from './i18n'\nimport QuestionPane from './QuestionPane.vue'\nimport type { QuestionCategoryRef, QuestionRow } from './types'\n\n/**\n * The questions: the whole FAQ on the left, the one being written on the right (§4.5).\n *\n * The list and the form side by side rather than a page per question, because a question is\n * written in a minute and the next one is usually the one under it: a round trip through a\n * page of its own for every question is most of the time spent. On a phone the form is the\n * whole screen, in the drawer `WxListDetail` raises, and draws its own way back.\n *\n * The order is decision 4, as with services: without a filter a drag moves the common order;\n * narrowed to one category, the category's own; a search or the bin shows a selection, so the\n * grips go away and the line under the list says why.\n *\n * Everything the screen is looking at lives in the address — the view, the category, the\n * search and the open question — so a link to a question is a link to it in its list.\n */\nwithDefaults(defineProps<{ base?: string }>(), { base: '/faq' })\n\nconst context = useAdmin()\nconst api = createFaqApi(context)\nconst route = useRoute()\nconst router = useRouter()\nuseFaqMessages()\n\nconst t = useTranslate('webx-faq')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst rows = ref<QuestionRow[]>([])\nconst categories = ref<QuestionCategoryRef[]>([])\nconst loading = ref(true)\n\nconst canManage = computed(() => context.can('faq.manage'))\n\nconst title = computed(\n () =>\n context.state.manifest?.modules.find((module) => module.id === 'faq')?.title ??\n t('module.questions'),\n)\n\nconst view = computed<TabValue>({\n get: () => (route.query.view === 'trashed' ? 'trashed' : ''),\n set: (value) => {\n const query: LocationQueryRaw = {\n ...route.query,\n view: value === '' ? undefined : String(value),\n }\n\n // A question from the list is not a question from the bin: the form closes with the view.\n delete query.question\n void router.replace({ query })\n },\n})\n\nconst inBin = computed(() => view.value === 'trashed')\n\nconst category = computed<number | null>(() => {\n const raw = route.query.category\n\n return typeof raw === 'string' && raw !== '' ? Number(raw) || null : null\n})\n\nconst q = computed(() => (typeof route.query.q === 'string' ? route.query.q : ''))\n\n/* What is typed, ahead of the address: the address follows it after a pause. */\nconst typed = ref(q.value)\n\n/** The open question: a number, `'new'` for one not yet saved, or nothing. */\nconst selected = computed<number | 'new' | null>(() => {\n const raw = route.query.question\n\n if (raw === 'new') return canManage.value ? 'new' : null\n\n return typeof raw === 'string' && raw !== '' ? Number(raw) || null : null\n})\n\n/*\n * Open is \"a question is chosen\", and closing is un-choosing it — so the address, the list's\n * highlight and the drawer on a phone can never disagree about it.\n */\nconst open = computed({\n get: () => selected.value !== null,\n set: (value) => {\n if (!value) close()\n },\n})\n\nconst views = computed<TabItem[]>(() => [\n { value: '', label: t('question.all') },\n { value: 'trashed', label: t('question.bin'), icon: 'trash' },\n])\n\nconst order = useItemOrder(FAQ_API, () => ({\n q: q.value,\n category: category.value,\n filtered: inBin.value,\n}))\n\n/* Dragged only by somebody who may write, and only in a list that is a whole order. */\nconst sortable = computed(() => canManage.value && order.sortable.value)\n\nconst emptyText = computed(() => {\n if (q.value !== '') return t('question.empty-search')\n\n return inBin.value ? t('question.empty-bin') : t('question.empty')\n})\n\nasync function load(): Promise<void> {\n loading.value = true\n\n try {\n const answer = await api.list({\n search: q.value,\n category: inBin.value ? null : category.value,\n trashed: inBin.value,\n })\n\n rows.value = answer.data\n categories.value = answer.filters.categories\n } catch (error) {\n toast.danger(message(error))\n } finally {\n loading.value = false\n }\n}\n\nwatch(\n () => [view.value, category.value, q.value] as const,\n () => void load(),\n)\nvoid load()\n\nlet pause: ReturnType<typeof setTimeout> | undefined\n\nwatch(typed, (value) => {\n clearTimeout(pause)\n pause = setTimeout(() => {\n void router.replace({ query: { ...route.query, q: value.trim() === '' ? undefined : value } })\n }, 300)\n})\n\nonBeforeUnmount(() => clearTimeout(pause))\n\nfunction narrow(value: unknown): void {\n void router.replace({\n query: { ...route.query, category: typeof value === 'number' ? String(value) : undefined },\n })\n}\n\n/* `replace` and not `push`: going through twenty questions is not twenty steps of history. */\nfunction choose(id: number | 'new'): void {\n void router.replace({ query: { ...route.query, question: String(id) } })\n}\n\nfunction close(): void {\n const query = { ...route.query }\n\n delete query.question\n void router.replace({ query })\n}\n\n/** A new question was saved: it is a record now, and the list has it. */\nfunction created(row: QuestionRow): void {\n choose(row.id)\n void load()\n}\n\n/**\n * The order on screen, written as a whole — the list or the category's, whichever this is. The\n * list is already in its new order (`WxSortableList` reorders the model before it says so), so a\n * failure has to put it back rather than leave the screen disagreeing with the database.\n */\nasync function reorder(): Promise<void> {\n try {\n await order.move(rows.value.map((row) => row.id))\n } catch (error) {\n toast.danger(message(error, t('question.reorder-failed')))\n await load()\n }\n}\n\nfunction actionsFor(row: QuestionRow): RowAction[] {\n if (!canManage.value) return []\n\n if (inBin.value) {\n return [\n {\n key: 'restore',\n icon: 'refresh',\n label: t('question.restore'),\n run: () => void restore(row),\n },\n ]\n }\n\n return [\n {\n key: 'copy',\n icon: 'copy',\n label: t('question.copy-link'),\n run: () => void copyLink(row),\n },\n {\n key: 'delete',\n icon: 'trash',\n label: t('question.delete'),\n danger: true,\n run: () => void remove(row),\n },\n ]\n}\n\nasync function remove(row: QuestionRow): Promise<void> {\n const agreed = await confirm({\n title: t('question.delete-title', { title: row.question }),\n message: t('question.delete-text'),\n confirmText: t('question.delete'),\n cancelText: t('question.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.remove(row.id)\n toast.success(t('question.deleted'))\n removed(row.id)\n } catch (error) {\n toast.danger(message(error))\n }\n}\n\n/** Gone from the list; and if it was the open one, the form goes with it. */\nfunction removed(id: number): void {\n if (selected.value === id) close()\n\n void load()\n}\n\nasync function restore(row: QuestionRow): Promise<void> {\n try {\n await api.restore(row.id)\n toast.success(t('question.restored'))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n await load()\n }\n}\n\nasync function copyLink(row: QuestionRow): Promise<void> {\n try {\n await navigator.clipboard.writeText(`#${row.anchor}`)\n toast.success(t('question.link-copied'))\n } catch {\n // A browser that refuses the clipboard without a gesture it trusts.\n }\n}\n\n/* Published and yet shown in no language: the one state the list has to point out (decision 9). */\nfunction unseen(row: QuestionRow): boolean {\n return row.published && row.locales.length === 0\n}\n</script>\n\n<template>\n <wx-list-screen v-model:view=\"view\" :title=\"title\" :views=\"views\" :card=\"false\">\n <wx-card class=\"wx-faq\" padding=\"none\">\n <wx-list-detail\n v-model:open=\"open\"\n class=\"wx-faq__panes\"\n :list-width=\"360\"\n :detail-min=\"460\"\n :detail-label=\"t('module.questions')\"\n >\n <template #list>\n <div class=\"wx-faq__bar\">\n <wx-input\n v-model=\"typed\"\n class=\"wx-faq__search\"\n size=\"sm\"\n clearable\n :placeholder=\"t('question.search')\"\n :aria-label=\"t('question.search')\"\n >\n <template #prefix><wx-icon name=\"search\" size=\"sm\" /></template>\n </wx-input>\n\n <!-- Always in sight rather than behind a funnel: it is the one filter that decides\n which order a drag writes, and the list says so right under it. -->\n <wx-select\n v-if=\"!inBin\"\n :model-value=\"category\"\n :options=\"categories.map((item) => ({ value: item.id, label: item.title }))\"\n :placeholder=\"t('question.any-category')\"\n :aria-label=\"t('question.filter-category')\"\n clearable\n size=\"sm\"\n @update:model-value=\"narrow\"\n />\n </div>\n\n <!--\n A line of the list rather than a button in the head: the new question is written\n where it will stand, beside the ones it joins — and the form opens the same way any\n other question's does.\n -->\n <button\n v-if=\"canManage && !inBin\"\n type=\"button\"\n class=\"wx-faq__new\"\n :class=\"{ 'is-current': selected === 'new' }\"\n @click=\"choose('new')\"\n >\n <wx-icon name=\"plus\" size=\"sm\" />\n {{ t('question.new') }}\n </button>\n\n <wx-skeleton v-if=\"loading && rows.length === 0\" class=\"wx-faq__loading\" :rows=\"5\" />\n\n <wx-empty\n v-else-if=\"rows.length === 0\"\n :title=\"emptyText\"\n :description=\"!inBin && q === '' ? t('question.empty-help') : undefined\"\n />\n\n <template v-else>\n <!-- A grip and not the whole row: the row opens a question, and a row that both\n opens and drags is one where one of the two happens by accident. -->\n <wx-sortable-list\n v-model=\"rows\"\n class=\"wx-faq__list\"\n :class=\"{ 'is-loading': loading }\"\n plain\n item-key=\"id\"\n :handle=\"sortable ? 'grip' : 'row'\"\n :item-label=\"(item: QuestionRow) => item.question\"\n :disabled=\"!sortable\"\n @move=\"reorder\"\n >\n <template #default=\"{ item }\">\n <component\n :is=\"inBin ? 'div' : 'button'\"\n :type=\"inBin ? undefined : 'button'\"\n class=\"wx-faq-row\"\n :class=\"{ 'is-current': selected === item.id }\"\n @click=\"inBin ? undefined : choose(item.id)\"\n >\n <span class=\"wx-faq-row__question\">{{ item.question }}</span>\n\n <span class=\"wx-faq-row__facts\">\n <wx-badge v-if=\"!item.published\" dot size=\"sm\">\n {{ t('question.not-published') }}\n </wx-badge>\n <wx-tooltip v-else-if=\"unseen(item)\" :content=\"t('question.seen-nowhere')\">\n <wx-badge type=\"warning\" dot size=\"sm\">\n {{ t('question.not-published') }}\n </wx-badge>\n </wx-tooltip>\n\n <wx-badge v-for=\"chip in item.categories\" :key=\"chip.id\" size=\"sm\" round>\n {{ chip.title }}\n </wx-badge>\n\n <span v-if=\"item.locales.length > 0\" class=\"wx-faq-row__locales\">\n {{ item.locales.join(' · ') }}\n </span>\n </span>\n </component>\n </template>\n\n <template #actions=\"{ item }\">\n <wx-row-menu :actions=\"actionsFor(item)\" :label=\"item.question\" />\n </template>\n </wx-sortable-list>\n\n <!-- What a drag does here, or why there is nothing to drag. -->\n <wx-alert\n v-if=\"canManage && !inBin\"\n class=\"wx-faq__note\"\n type=\"info\"\n :description=\"order.hint.value\"\n />\n </template>\n </template>\n\n <template #detail=\"{ inline, back }\">\n <question-pane\n v-if=\"selected !== null\"\n :id=\"selected === 'new' ? null : selected\"\n :key=\"String(selected)\"\n :category=\"category\"\n :inline=\"inline\"\n @back=\"back\"\n @saved=\"load\"\n @created=\"created\"\n @removed=\"removed\"\n />\n </template>\n\n <template #empty>\n <wx-empty\n :title=\"t('question.choose')\"\n :description=\"canManage ? t('question.choose-help') : undefined\"\n />\n </template>\n </wx-list-detail>\n </wx-card>\n </wx-list-screen>\n</template>\n\n<style>\n/*\n * What scrolls here is the page, the way it does on every other list of the panel. The card's\n * corners are the screen's, and the panes paint their own background to the edge: `clip`, not\n * `hidden`, which would make this a scroll container and pin everything sticky inside to a box\n * that never moves (CLAUDE.md §4).\n */\n.wx-faq > .wx-card__body {\n display: flex;\n flex-direction: column;\n overflow: clip;\n border-radius: inherit;\n}\n</style>\n\n<style scoped>\n.wx-faq__bar {\n display: flex;\n flex-wrap: wrap;\n gap: var(--wx-space-8);\n padding: var(--wx-space-12) var(--wx-space-12) var(--wx-space-8);\n}\n\n.wx-faq__search {\n flex: 1 1 180px;\n min-width: 0;\n}\n\n/* The select is a fragment (it carries its dropdown beside it), so our scope attribute never\n reaches it: styled from our own wrapper (CLAUDE.md §4). */\n.wx-faq__bar > :deep(.wx-select) {\n flex: 1 1 140px;\n min-width: 0;\n}\n\n.wx-faq__new {\n display: flex;\n align-items: center;\n gap: var(--wx-space-8);\n width: calc(100% - 2 * var(--wx-space-8));\n margin: 0 var(--wx-space-8) var(--wx-space-4);\n padding: var(--wx-space-8);\n border: 1px dashed var(--wx-border-default);\n border-radius: var(--wx-radius-sm);\n background: none;\n font: inherit;\n color: var(--wx-color-primary);\n text-align: start;\n cursor: pointer;\n}\n\n.wx-faq__new:hover,\n.wx-faq__new.is-current {\n background: var(--wx-bg-subtle);\n}\n\n.wx-faq__loading {\n padding: var(--wx-space-12);\n}\n\n.wx-faq__list {\n padding-inline: var(--wx-space-8);\n}\n\n.wx-faq__list.is-loading {\n opacity: 0.6;\n}\n\n.wx-faq__list :deep(.wx-sortable-list__row) {\n gap: var(--wx-space-4);\n padding-inline: var(--wx-space-4);\n border-radius: var(--wx-radius-sm);\n}\n\n.wx-faq__list :deep(.wx-sortable-list__row:hover),\n.wx-faq__list :deep(.wx-sortable-list__row:has(.is-current)) {\n background: var(--wx-bg-subtle);\n}\n\n.wx-faq__note {\n margin: var(--wx-space-8);\n}\n\n.wx-faq-row {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: var(--wx-space-4);\n /* The row is the target: a short question should not leave half the line dead — and a\n button, unlike a link, is only as wide as what is in it until it is told otherwise. */\n flex: 1 1 auto;\n width: 100%;\n min-width: 0;\n padding-block: var(--wx-space-8);\n padding-inline: 0;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: start;\n}\n\nbutton.wx-faq-row {\n cursor: pointer;\n}\n\n.wx-faq-row.is-current .wx-faq-row__question {\n color: var(--wx-color-primary);\n}\n\n/* Two lines, then an ellipsis: a question is a sentence, and one line of it is rarely enough to\n tell two apart. */\n.wx-faq-row__question {\n display: -webkit-box;\n overflow: hidden;\n font-weight: var(--wx-font-weight-medium);\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow-wrap: anywhere;\n}\n\n.wx-faq-row__facts {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--wx-space-4);\n min-width: 0;\n}\n\n.wx-faq-row__facts:empty {\n display: none;\n}\n\n.wx-faq-row__locales {\n color: var(--wx-text-muted);\n font-family: var(--wx-font-family-mono);\n font-size: var(--wx-font-size-xs);\n text-transform: uppercase;\n}\n</style>\n","import { categoryRoutes, type AdminModule, type CategoriesOptions } from '@webx-ui/module-admin'\nimport AnchorField from './AnchorField.vue'\nimport QuestionsPage from './QuestionsPage.vue'\n\nexport interface FaqOptions {\n /** Where the questions live inside the panel. The categories sit under it. */\n path?: string\n}\n\n/**\n * The categories of the FAQ as the panel's shared category screens see them. Exported so that a\n * panel mounting the screens somewhere of its own does not have to repeat the words.\n *\n * No words about addresses: a category here has none (decision 2) — it is a button of the filter\n * on the site, and the shared list leaves the address line out when the server says so.\n */\nexport function faqCategoriesOptions(path = '/faq'): CategoriesOptions {\n return {\n api: 'faq/categories',\n path: `${path}/categories`,\n name: 'webx.faq.categories',\n module: 'faq-categories',\n screen: 'faq.category-form',\n manage: 'faq.categories.manage',\n count: 'questions_count',\n // The list narrowed to one category is also where its own order is dragged.\n items: (id) => ({ path, query: { category: String(id) } }),\n words: {\n new: 'webx-faq::category.new',\n empty: 'webx-faq::category.empty',\n 'empty-help': 'webx-faq::category.empty-help',\n order: 'webx-faq::category.order',\n hidden: 'webx-faq::category.hidden',\n count: 'webx-faq::category.questions',\n 'show-items': 'webx-faq::category.show-questions',\n 'delete-blocked': 'webx-faq::category.delete-blocked',\n 'delete-text': 'webx-faq::category.delete-text',\n deleted: 'webx-faq::category.deleted',\n saved: 'webx-faq::category.saved',\n 'field-title': 'webx-faq::category.field-title',\n },\n }\n}\n\n/**\n * The FAQ as sections of the panel: questions, and their categories (§4.5).\n *\n * Two modules rather than one, because the navigation is one entry per module; the server puts\n * both in the `faq` group, which is what draws them under one heading. A section whose server\n * half is not installed never appears — the entry is built from the manifest.\n */\nexport function faq(options: FaqOptions = {}): AdminModule[] {\n const path = options.path ?? '/faq'\n\n return [\n {\n id: 'faq',\n path,\n // One route: the open question is in the address (`?question=`), beside the list it was\n // opened from, so a link to it is a link to both.\n routes: [{ path, name: 'webx.faq', component: QuestionsPage, props: { base: path } }],\n /*\n * The one node of `faq.form` only this module can draw. A field in the screen's eyes — it\n * gets the label and the help of its node — but without a name, so nothing is bound.\n */\n types: {\n 'wx-faq-anchor': { component: AnchorField, kind: 'field' },\n },\n },\n {\n id: 'faq-categories',\n path: `${path}/categories`,\n routes: categoryRoutes(faqCategoriesOptions(path)),\n },\n ]\n}\n"],"names":["questionEditorKey","provideQuestionEditor","editor","provide","useQuestionEditor","inject","faqMessages","seeded","useFaqMessages","i18n","useAdmin","t","useTranslate","anchor","computed","fragment","copy","toast","_openBlock","_createElementBlock","_hoisted_1","_Fragment","_createVNode","_unref","WxText","WxButton","WxIcon","_createTextVNode","_createBlock","FAQ_API","createFaqApi","admin","base","data","body","query","search","id","values","ids","category","reorderItems","props","__props","emit","__emit","context","api","locales","useLocales","message","useErrorText","question","ref","snapshot","loading","saving","errors","canManage","current","dirty","state","title","written","fallback","localizedValue","take","detail","blank","load","error","save","made","remove","row","confirm","leaveGuard","event","onKeydown","useBodyKeys","onMounted","onBeforeUnmount","mayLeave","onBeforeRouteUpdate","to","from","onBeforeRouteLeave","actions","WxSkeleton","WxScreenHead","WxBadge","WxScreen","$event","WxActionBar","WxSaveState","route","useRoute","router","useRouter","rows","categories","module","view","value","inBin","raw","q","typed","selected","open","close","views","order","useItemOrder","sortable","emptyText","answer","watch","pause","narrow","choose","created","reorder","actionsFor","restore","copyLink","removed","unseen","WxListScreen","WxCard","WxListDetail","_createElementVNode","WxInput","WxSelect","item","_normalizeClass","WxEmpty","WxSortableList","_withCtx","_resolveDynamicComponent","_hoisted_2","_toDisplayString","_hoisted_3","WxTooltip","_renderList","chip","_hoisted_4","WxRowMenu","WxAlert","inline","back","QuestionPane","faqCategoriesOptions","path","faq","options","QuestionsPage","AnchorField","categoryRoutes"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/editor.ts","../src/messages.ts","../src/i18n.ts","../src/AnchorField.vue","../src/api.ts","../src/QuestionPane.vue","../src/QuestionsPage.vue","../src/module.ts"],"sourcesContent":["import { inject, provide, type InjectionKey, type Ref } from 'vue'\nimport type { QuestionRow } from './types'\n\n/**\n * What the form knows and the nodes of `faq.form` do not: the question being edited.\n *\n * The anchor is not a value of the screen — nobody writes it, it is made once on the server and\n * never changes (decision 10) — so the node that shows it (`wx-faq-anchor`) reads the record.\n */\nexport interface QuestionEditorContext {\n /** The question as the server last answered it; `null` while it is new and not yet saved. */\n question: Ref<QuestionRow | null>\n}\n\nexport const questionEditorKey: InjectionKey<QuestionEditorContext> = Symbol('wx-faq-question')\n\nexport function provideQuestionEditor(editor: QuestionEditorContext): void {\n provide(questionEditorKey, editor)\n}\n\n/** The form above this node, or `null` outside one — a demo or a test draws the empty state. */\nexport function useQuestionEditor(): QuestionEditorContext | null {\n return inject(questionEditorKey, null)\n}\n","import type { Messages } from '@webx-ui/module-admin'\n\n/**\n * The English this package speaks on its own, before the server's dictionary arrives, or without\n * one. The server ships the same lines in ten languages under `webx-faq::` and overrides these;\n * `messages.test.ts` keeps the two sets of keys equal.\n */\nexport const faqMessages: Record<string, Messages> = {\n module: {\n group: 'FAQ',\n questions: 'Questions',\n categories: 'Categories',\n },\n question: {\n new: 'New question',\n untitled: 'Untitled',\n search: 'Search the questions',\n empty: 'No questions yet.',\n 'empty-help': 'A question reaches the site in a FAQ block, on any page.',\n 'empty-search': 'Nothing matches that.',\n 'empty-bin': 'The bin is empty.',\n all: 'All',\n bin: 'Bin',\n 'filter-category': 'Category',\n 'any-category': 'Any category',\n 'not-published': 'Not published',\n 'seen-nowhere': 'Not seen in any language: it needs a question and an answer in one.',\n choose: 'Choose a question',\n 'choose-help': 'Or write a new one: the first line of the list.',\n save: 'Save',\n saved: 'The question is saved.',\n 'save-failed': 'The question was not saved.',\n cancel: 'Cancel',\n delete: 'Delete',\n 'delete-title': 'Delete “:title”?',\n 'delete-text':\n 'It goes to the bin and leaves every block on the site. Restored, it comes back with the same link.',\n deleted: 'The question is in the bin.',\n restore: 'Restore',\n restored: 'The question is back.',\n 'copy-link': 'Copy the link',\n 'link-copied': 'The link is on the clipboard.',\n 'anchor-later': 'Made from the question when it is first saved.',\n 'reorder-failed': 'The new order was not saved.',\n 'leave-title': 'Leave without saving?',\n 'leave-text': 'What you wrote in this question is not on the server.',\n leave: 'Leave',\n back: 'Back to the list',\n },\n screen: {\n question: 'Question',\n answer: 'Answer',\n 'answer-help':\n 'A question is shown in a language only when it has both the question and the answer in it.',\n settings: 'Settings',\n published: 'Published',\n 'published-help':\n 'On the site in every block that shows its categories, from the moment it is saved.',\n categories: 'Categories',\n 'categories-help': 'What a block picks the question by, and the buttons of its filter.',\n anchor: 'Link',\n 'anchor-help': 'Made once from the question and never changed, so links to it keep working.',\n },\n category: {\n 'field-title': 'Title',\n visible: 'Shown on the site',\n 'visible-help':\n 'Hidden, it drops out of every filter; its questions stay in the blocks that show them.',\n new: 'New category',\n empty: 'No categories yet.',\n 'empty-help':\n 'A category groups questions: a block shows the ones you pick, and its filter has a button for each.',\n order: 'Drag to change the order of the filter buttons on the site.',\n hidden: 'Hidden from the site',\n questions: 'Questions: :count',\n 'show-questions': 'Show its questions',\n 'delete-blocked': 'This category still holds questions. Move them first.',\n 'delete-text': 'It goes to the bin and its button leaves every filter. Its questions stay.',\n deleted: 'The category is in the bin.',\n saved: 'The category is saved.',\n },\n}\n","import { useAdmin } from '@webx-ui/module-admin'\nimport { faqMessages } from './messages'\n\nconst seeded = new WeakSet<object>()\n\n/** Puts this package's English under the panel's dictionary, once per panel. */\nexport function useFaqMessages(): void {\n try {\n const { i18n } = useAdmin()\n\n if (!seeded.has(i18n)) {\n i18n.defaults('webx-faq', faqMessages)\n seeded.add(i18n)\n }\n } catch {\n // Outside a panel there is no dictionary to seed, and `useTranslate` falls back on its own.\n }\n}\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { toast, WxButton, WxIcon, WxText } from '@webx-ui/core'\nimport { useTranslate } from '@webx-ui/module-admin'\nimport { useQuestionEditor } from './editor'\nimport { useFaqMessages } from './i18n'\n\n/**\n * The anchor of a question: shown, never written (decision 10).\n *\n * Registered as a field only so that the screen draws it the way it draws a field — the label and\n * the help of its node around it. It has no name, so there is no value to bind, and it reads the\n * anchor off the question the form is editing.\n *\n * What is copied is the fragment, `#anchor`, and not an address: a question has no page of its\n * own, and the page it is linked on is whichever page the editor put the block on.\n */\ndefineOptions({ name: 'WxFaqAnchor' })\n\nconst editor = useQuestionEditor()\nuseFaqMessages()\n\nconst t = useTranslate('webx-faq')\n\nconst anchor = computed(() => editor?.question.value?.anchor ?? '')\nconst fragment = computed(() => (anchor.value === '' ? '' : `#${anchor.value}`))\n\nasync function copy(): Promise<void> {\n try {\n await navigator.clipboard.writeText(fragment.value)\n toast.success(t('question.link-copied'))\n } catch {\n // A browser that refuses the clipboard without a gesture it trusts; the anchor is on screen.\n }\n}\n</script>\n\n<template>\n <div class=\"wx-faq-anchor\">\n <template v-if=\"fragment !== ''\">\n <wx-text class=\"wx-faq-anchor__value\" mono truncate>{{ fragment }}</wx-text>\n <wx-button variant=\"outline\" size=\"sm\" @click=\"copy\">\n <template #icon><wx-icon name=\"copy\" /></template>\n {{ t('question.copy-link') }}\n </wx-button>\n </template>\n\n <wx-text v-else size=\"sm\" tone=\"muted\">{{ t('question.anchor-later') }}</wx-text>\n </div>\n</template>\n\n<style scoped>\n.wx-faq-anchor {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--wx-space-8);\n min-width: 0;\n}\n\n.wx-faq-anchor__value {\n flex: 1 1 auto;\n min-width: 0;\n}\n</style>\n","import { reorderItems, type AdminContext } from '@webx-ui/module-admin'\nimport type { ScreenModel } from '@webx-ui/schema'\nimport type { QuestionDetail, QuestionQuery, QuestionRow, QuestionsList } from './types'\n\nexport interface FaqApi {\n /** Every question, with the categories the list can be narrowed to. */\n list(query?: QuestionQuery): Promise<QuestionsList>\n get(id: number): Promise<QuestionDetail>\n /**\n * A new question out of the values of its form, in one request: the form is filled in first\n * and the record made on the first save, so nobody leaves an empty question behind.\n */\n create(values: ScreenModel): Promise<QuestionDetail>\n /** Refused with a 422 under the name of the field. */\n save(id: number, values: ScreenModel): Promise<QuestionDetail>\n remove(id: number): Promise<void>\n restore(id: number): Promise<QuestionRow>\n /** The order on screen: of the whole list, or — with `category` — of that category alone. */\n reorder(ids: number[], category?: number | null): Promise<void>\n}\n\n/** The path of the list under the panel's API, and of its order (`CategoryRoutes::items()`). */\nexport const FAQ_API = 'faq/questions'\n\n/**\n * Everything under `/faq/questions`, below the panel's API path — except the categories, which\n * are the panel's shared ones and are asked for with `createCategoriesApi(admin, 'faq/categories')`.\n */\nexport function createFaqApi(admin: AdminContext): FaqApi {\n const base = `${admin.apiPath}/${FAQ_API}`\n const data = <T>(body: { data: T }): T => body.data\n\n return {\n list: (query = {}) => {\n const search = new URLSearchParams()\n\n // Only what was asked for: `category=` would ask for the questions of no category.\n if (query.search) search.set('search', query.search)\n if (query.category != null) search.set('category', String(query.category))\n if (query.trashed) search.set('trashed', '1')\n\n return admin.http.get<QuestionsList>(search.size > 0 ? `${base}?${search}` : base)\n },\n get: (id) => admin.http.get<{ data: QuestionDetail }>(`${base}/${id}`).then(data),\n create: (values) => admin.http.post<{ data: QuestionDetail }>(base, { values }).then(data),\n save: (id, values) =>\n admin.http.put<{ data: QuestionDetail }>(`${base}/${id}`, { values }).then(data),\n remove: (id) => admin.http.delete<void>(`${base}/${id}`).then(() => undefined),\n restore: (id) => admin.http.post<{ data: QuestionRow }>(`${base}/${id}/restore`, {}).then(data),\n reorder: (ids, category = null) => reorderItems(admin, FAQ_API, ids, category),\n }\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref } from 'vue'\nimport { onBeforeRouteLeave, onBeforeRouteUpdate, type RouteLocationNormalized } from 'vue-router'\nimport {\n confirm,\n localizedValue,\n toast,\n useLocales,\n WxActionBar,\n WxBadge,\n WxButton,\n WxSkeleton,\n type LocalizedValue,\n} from '@webx-ui/core'\nimport {\n useAdmin,\n useErrorText,\n useTranslate,\n WxSaveState,\n WxScreen,\n WxScreenHead,\n type ScreenAction,\n useBodyKeys,\n} from '@webx-ui/module-admin'\nimport type { ScreenModel } from '@webx-ui/schema'\nimport { createFaqApi } from './api'\nimport { provideQuestionEditor } from './editor'\nimport { useFaqMessages } from './i18n'\nimport type { QuestionDetail, QuestionRow } from './types'\n\n/**\n * One question: the pane beside the list, and the whole screen on a phone.\n *\n * The described screen `faq.form` with a head over it and one button under it. One explicit\n * save, and not the autosave of an article: a question has no draft (decision 12), so what is\n * saved is on the site at once — the button is the moment somebody decides it is ready.\n *\n * A new question is a form first and a record on the first save: an empty row made by pressing\n * \"New question\" and walking away would be a question with nothing in it on the list for ever.\n *\n * The way back is drawn here and not by the pane around it: the drawer a narrow screen opens\n * this in has no close of its own on purpose (CLAUDE.md §4).\n */\ndefineOptions({ name: 'WxFaqQuestionPane' })\n\nconst props = withDefaults(\n defineProps<{\n /** `null` — a question not written yet. */\n id: number | null\n /** The category the list is narrowed to: a new question starts in it. */\n category?: number | null\n /** False in the drawer a narrow screen opens this in. */\n inline?: boolean\n }>(),\n { category: null, inline: true },\n)\n\nconst emit = defineEmits<{\n back: []\n /** The question was written; the list redraws its row. */\n saved: [row: QuestionRow]\n /** The first save of a new question: it is a record now, with an id to be opened by. */\n created: [row: QuestionRow]\n removed: [id: number]\n}>()\n\nconst context = useAdmin()\nconst api = createFaqApi(context)\nconst locales = useLocales()\nuseFaqMessages()\n\nconst t = useTranslate('webx-faq')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst question = ref<QuestionRow | null>(null)\nconst values = ref<ScreenModel>({})\nconst snapshot = ref('')\n\nconst loading = ref(props.id !== null)\nconst saving = ref(false)\nconst errors = ref<Record<string, string[]>>({})\n\nconst canManage = computed(() => context.can('faq.manage'))\n\nconst current = computed(() => JSON.stringify(values.value))\nconst dirty = computed(() => snapshot.value !== '' && current.value !== snapshot.value)\n\nconst state = computed<'saving' | 'unsaved' | 'saved'>(() => {\n if (saving.value) return 'saving'\n\n return dirty.value || props.id === null ? 'unsaved' : 'saved'\n})\n\n/** The question follows the field rather than the answer: an edit shows at the top as it is typed. */\nconst title = computed(() => {\n const written = values.value.question as LocalizedValue | string | null | undefined\n const fallback = props.id === null ? t('question.new') : (question.value?.question ?? '')\n\n return localizedValue(written, locales.active.value, fallback) || t('question.untitled')\n})\n\nprovideQuestionEditor({ question })\n\nfunction take(detail: QuestionDetail): void {\n question.value = detail.question\n values.value = detail.values\n snapshot.value = JSON.stringify(detail.values)\n}\n\nif (props.id === null) {\n // What the server would answer for a question with nothing in it yet.\n const blank: ScreenModel = {\n question: {},\n answer: {},\n published: false,\n categories: props.category === null ? [] : [props.category],\n }\n\n values.value = blank\n snapshot.value = JSON.stringify(blank)\n}\n\nasync function load(): Promise<void> {\n if (props.id === null) return\n\n loading.value = true\n\n try {\n take(await api.get(props.id))\n } catch (error) {\n toast.danger(message(error))\n emit('back')\n } finally {\n loading.value = false\n }\n}\n\nasync function save(): Promise<void> {\n if (saving.value || !canManage.value) return\n\n saving.value = true\n errors.value = {}\n\n try {\n const made = props.id === null\n const detail = made\n ? await api.create(values.value)\n : await api.save(props.id as number, values.value)\n\n take(detail)\n toast.success(t('question.saved'))\n if (made) emit('created', detail.question)\n else emit('saved', detail.question)\n } catch (error) {\n const body = (error as { body?: { errors?: Record<string, string[]> } }).body\n\n if (body?.errors) {\n errors.value = body.errors\n toast.danger(t('question.save-failed'))\n } else {\n toast.danger(message(error))\n }\n } finally {\n saving.value = false\n }\n}\n\nasync function remove(): Promise<void> {\n const row = question.value\n\n if (!row) return\n\n const agreed = await confirm({\n title: t('question.delete-title', { title: row.question }),\n message: t('question.delete-text'),\n confirmText: t('question.delete'),\n cancelText: t('question.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.remove(row.id)\n toast.success(t('question.deleted'))\n // Nothing left to protect: the record is in the bin.\n snapshot.value = current.value\n emit('removed', row.id)\n } catch (error) {\n toast.danger(message(error))\n }\n}\n\n/* The browser's own guard: it cannot wait for a request, so all it can do is ask. */\nfunction leaveGuard(event: BeforeUnloadEvent): void {\n if (dirty.value) event.preventDefault()\n}\n\n/* Ctrl+S saves, as it does everywhere else that has a save button. */\nfunction onKeydown(event: KeyboardEvent): void {\n if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {\n event.preventDefault()\n if (dirty.value || props.id === null) void save()\n }\n}\n\n// And from the page around the form, where a click on empty space leaves the focus.\nuseBodyKeys(onKeydown)\n\nonMounted(() => {\n void load()\n window.addEventListener('beforeunload', leaveGuard)\n})\n\nonBeforeUnmount(() => {\n window.removeEventListener('beforeunload', leaveGuard)\n})\n\nasync function mayLeave(): Promise<boolean> {\n if (!dirty.value || !canManage.value) return true\n\n return await confirm({\n title: t('question.leave-title'),\n message: t('question.leave-text'),\n confirmText: t('question.leave'),\n cancelText: t('question.cancel'),\n tone: 'danger',\n })\n}\n\n/*\n * The list and this form are one route, so another question is an update of it rather than a\n * way out — and so is every keystroke in the search above the list, which goes into the address\n * too. Only a change of the question asks.\n */\nonBeforeRouteUpdate(async (to: RouteLocationNormalized, from: RouteLocationNormalized) =>\n to.query.question === from.query.question ? true : await mayLeave(),\n)\n\nonBeforeRouteLeave(mayLeave)\n\n/* What leads away from the question. On a narrow pane the head folds them into the ···. */\nconst actions = computed<ScreenAction[]>(() =>\n canManage.value && question.value !== null\n ? [\n {\n key: 'delete',\n label: t('question.delete'),\n icon: 'trash',\n danger: true,\n menu: true,\n run: () => void remove(),\n },\n ]\n : [],\n)\n</script>\n\n<template>\n <div class=\"wx-faq-question\" @keydown=\"onKeydown\">\n <wx-skeleton v-if=\"loading\" class=\"wx-faq-question__ghost\" title :rows=\"6\" />\n\n <template v-else>\n <wx-screen-head\n :level=\"3\"\n :back=\"!props.inline\"\n :back-label=\"t('question.back')\"\n :title=\"title\"\n :actions=\"actions\"\n :collapse-below=\"480\"\n @back=\"emit('back')\"\n >\n <template v-if=\"question && !question.published\" #title-after>\n <wx-badge dot>{{ t('question.not-published') }}</wx-badge>\n </template>\n </wx-screen-head>\n\n <wx-screen\n v-model=\"values\"\n name=\"faq.form\"\n :errors=\"errors\"\n :disabled=\"!canManage || saving\"\n />\n\n <wx-action-bar v-if=\"canManage\">\n <template #state>\n <wx-save-state :state=\"state\" />\n </template>\n\n <wx-button\n type=\"primary\"\n :loading=\"saving\"\n :disabled=\"!dirty && props.id !== null\"\n @click=\"save\"\n >\n {{ t('question.save') }}\n </wx-button>\n </wx-action-bar>\n </template>\n </div>\n</template>\n\n<style scoped>\n.wx-faq-question {\n display: flex;\n flex-direction: column;\n gap: var(--wx-gap, var(--wx-space-16));\n min-width: 0;\n padding: var(--wx-gap, var(--wx-space-16));\n container-type: inline-size;\n}\n\n.wx-faq-question__ghost {\n max-width: 480px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, watch } from 'vue'\nimport { useRoute, useRouter, type LocationQueryRaw } from 'vue-router'\nimport {\n confirm,\n toast,\n WxAlert,\n WxBadge,\n WxCard,\n WxEmpty,\n WxIcon,\n WxInput,\n WxListDetail,\n WxSelect,\n WxSkeleton,\n WxSortableList,\n WxTooltip,\n type TabItem,\n type TabValue,\n} from '@webx-ui/core'\nimport {\n useAdmin,\n useErrorText,\n useItemOrder,\n useTranslate,\n WxListScreen,\n WxRowMenu,\n type RowAction,\n} from '@webx-ui/module-admin'\nimport { createFaqApi, FAQ_API } from './api'\nimport { useFaqMessages } from './i18n'\nimport QuestionPane from './QuestionPane.vue'\nimport type { QuestionCategoryRef, QuestionRow } from './types'\n\n/**\n * The questions: the whole FAQ on the left, the one being written on the right (§4.5).\n *\n * The list and the form side by side rather than a page per question, because a question is\n * written in a minute and the next one is usually the one under it: a round trip through a\n * page of its own for every question is most of the time spent. On a phone the form is the\n * whole screen, in the drawer `WxListDetail` raises, and draws its own way back.\n *\n * The order is decision 4, as with services: without a filter a drag moves the common order;\n * narrowed to one category, the category's own; a search or the bin shows a selection, so the\n * grips go away and the line under the list says why.\n *\n * Everything the screen is looking at lives in the address — the view, the category, the\n * search and the open question — so a link to a question is a link to it in its list.\n */\nwithDefaults(defineProps<{ base?: string }>(), { base: '/faq' })\n\nconst context = useAdmin()\nconst api = createFaqApi(context)\nconst route = useRoute()\nconst router = useRouter()\nuseFaqMessages()\n\nconst t = useTranslate('webx-faq')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst rows = ref<QuestionRow[]>([])\nconst categories = ref<QuestionCategoryRef[]>([])\nconst loading = ref(true)\n\nconst canManage = computed(() => context.can('faq.manage'))\n\nconst title = computed(\n () =>\n context.state.manifest?.modules.find((module) => module.id === 'faq')?.title ??\n t('module.questions'),\n)\n\nconst view = computed<TabValue>({\n get: () => (route.query.view === 'trashed' ? 'trashed' : ''),\n set: (value) => {\n const query: LocationQueryRaw = {\n ...route.query,\n view: value === '' ? undefined : String(value),\n }\n\n // A question from the list is not a question from the bin: the form closes with the view.\n delete query.question\n void router.replace({ query })\n },\n})\n\nconst inBin = computed(() => view.value === 'trashed')\n\nconst category = computed<number | null>(() => {\n const raw = route.query.category\n\n return typeof raw === 'string' && raw !== '' ? Number(raw) || null : null\n})\n\nconst q = computed(() => (typeof route.query.q === 'string' ? route.query.q : ''))\n\n/* What is typed, ahead of the address: the address follows it after a pause. */\nconst typed = ref(q.value)\n\n/** The open question: a number, `'new'` for one not yet saved, or nothing. */\nconst selected = computed<number | 'new' | null>(() => {\n const raw = route.query.question\n\n if (raw === 'new') return canManage.value ? 'new' : null\n\n return typeof raw === 'string' && raw !== '' ? Number(raw) || null : null\n})\n\n/*\n * Open is \"a question is chosen\", and closing is un-choosing it — so the address, the list's\n * highlight and the drawer on a phone can never disagree about it.\n */\nconst open = computed({\n get: () => selected.value !== null,\n set: (value) => {\n if (!value) close()\n },\n})\n\nconst views = computed<TabItem[]>(() => [\n { value: '', label: t('question.all') },\n { value: 'trashed', label: t('question.bin'), icon: 'trash' },\n])\n\nconst order = useItemOrder(FAQ_API, () => ({\n q: q.value,\n category: category.value,\n filtered: inBin.value,\n}))\n\n/* Dragged only by somebody who may write, and only in a list that is a whole order. */\nconst sortable = computed(() => canManage.value && order.sortable.value)\n\nconst emptyText = computed(() => {\n if (q.value !== '') return t('question.empty-search')\n\n return inBin.value ? t('question.empty-bin') : t('question.empty')\n})\n\nasync function load(): Promise<void> {\n loading.value = true\n\n try {\n const answer = await api.list({\n search: q.value,\n category: inBin.value ? null : category.value,\n trashed: inBin.value,\n })\n\n rows.value = answer.data\n categories.value = answer.filters.categories\n } catch (error) {\n toast.danger(message(error))\n } finally {\n loading.value = false\n }\n}\n\nwatch(\n () => [view.value, category.value, q.value] as const,\n () => void load(),\n)\nvoid load()\n\nlet pause: ReturnType<typeof setTimeout> | undefined\n\nwatch(typed, (value) => {\n clearTimeout(pause)\n pause = setTimeout(() => {\n void router.replace({ query: { ...route.query, q: value.trim() === '' ? undefined : value } })\n }, 300)\n})\n\nonBeforeUnmount(() => clearTimeout(pause))\n\nfunction narrow(value: unknown): void {\n void router.replace({\n query: { ...route.query, category: typeof value === 'number' ? String(value) : undefined },\n })\n}\n\n/* `replace` and not `push`: going through twenty questions is not twenty steps of history. */\nfunction choose(id: number | 'new'): void {\n void router.replace({ query: { ...route.query, question: String(id) } })\n}\n\nfunction close(): void {\n const query = { ...route.query }\n\n delete query.question\n void router.replace({ query })\n}\n\n/** A new question was saved: it is a record now, and the list has it. */\nfunction created(row: QuestionRow): void {\n choose(row.id)\n void load()\n}\n\n/**\n * The order on screen, written as a whole — the list or the category's, whichever this is. The\n * list is already in its new order (`WxSortableList` reorders the model before it says so), so a\n * failure has to put it back rather than leave the screen disagreeing with the database.\n */\nasync function reorder(): Promise<void> {\n try {\n await order.move(rows.value.map((row) => row.id))\n } catch (error) {\n toast.danger(message(error, t('question.reorder-failed')))\n await load()\n }\n}\n\nfunction actionsFor(row: QuestionRow): RowAction[] {\n if (!canManage.value) return []\n\n if (inBin.value) {\n return [\n {\n key: 'restore',\n icon: 'refresh',\n label: t('question.restore'),\n run: () => void restore(row),\n },\n ]\n }\n\n return [\n {\n key: 'copy',\n icon: 'copy',\n label: t('question.copy-link'),\n run: () => void copyLink(row),\n },\n {\n key: 'delete',\n icon: 'trash',\n label: t('question.delete'),\n danger: true,\n run: () => void remove(row),\n },\n ]\n}\n\nasync function remove(row: QuestionRow): Promise<void> {\n const agreed = await confirm({\n title: t('question.delete-title', { title: row.question }),\n message: t('question.delete-text'),\n confirmText: t('question.delete'),\n cancelText: t('question.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.remove(row.id)\n toast.success(t('question.deleted'))\n removed(row.id)\n } catch (error) {\n toast.danger(message(error))\n }\n}\n\n/** Gone from the list; and if it was the open one, the form goes with it. */\nfunction removed(id: number): void {\n if (selected.value === id) close()\n\n void load()\n}\n\nasync function restore(row: QuestionRow): Promise<void> {\n try {\n await api.restore(row.id)\n toast.success(t('question.restored'))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n await load()\n }\n}\n\nasync function copyLink(row: QuestionRow): Promise<void> {\n try {\n await navigator.clipboard.writeText(`#${row.anchor}`)\n toast.success(t('question.link-copied'))\n } catch {\n // A browser that refuses the clipboard without a gesture it trusts.\n }\n}\n\n/* Published and yet shown in no language: the one state the list has to point out (decision 9). */\nfunction unseen(row: QuestionRow): boolean {\n return row.published && row.locales.length === 0\n}\n</script>\n\n<template>\n <wx-list-screen v-model:view=\"view\" :title=\"title\" :views=\"views\" :card=\"false\">\n <wx-card class=\"wx-faq\" padding=\"none\">\n <wx-list-detail\n v-model:open=\"open\"\n class=\"wx-faq__panes\"\n :list-width=\"360\"\n :detail-min=\"460\"\n :detail-label=\"t('module.questions')\"\n >\n <template #list>\n <div class=\"wx-faq__bar\">\n <wx-input\n v-model=\"typed\"\n class=\"wx-faq__search\"\n size=\"sm\"\n clearable\n :placeholder=\"t('question.search')\"\n :aria-label=\"t('question.search')\"\n >\n <template #prefix><wx-icon name=\"search\" size=\"sm\" /></template>\n </wx-input>\n\n <!-- Always in sight rather than behind a funnel: it is the one filter that decides\n which order a drag writes, and the list says so right under it. -->\n <wx-select\n v-if=\"!inBin\"\n :model-value=\"category\"\n :options=\"categories.map((item) => ({ value: item.id, label: item.title }))\"\n :placeholder=\"t('question.any-category')\"\n :aria-label=\"t('question.filter-category')\"\n clearable\n size=\"sm\"\n @update:model-value=\"narrow\"\n />\n </div>\n\n <!--\n A line of the list rather than a button in the head: the new question is written\n where it will stand, beside the ones it joins — and the form opens the same way any\n other question's does.\n -->\n <button\n v-if=\"canManage && !inBin\"\n type=\"button\"\n class=\"wx-faq__new\"\n :class=\"{ 'is-current': selected === 'new' }\"\n @click=\"choose('new')\"\n >\n <wx-icon name=\"plus\" size=\"sm\" />\n {{ t('question.new') }}\n </button>\n\n <wx-skeleton v-if=\"loading && rows.length === 0\" class=\"wx-faq__loading\" :rows=\"5\" />\n\n <wx-empty\n v-else-if=\"rows.length === 0\"\n :title=\"emptyText\"\n :description=\"!inBin && q === '' ? t('question.empty-help') : undefined\"\n />\n\n <template v-else>\n <!-- A grip and not the whole row: the row opens a question, and a row that both\n opens and drags is one where one of the two happens by accident. -->\n <wx-sortable-list\n v-model=\"rows\"\n class=\"wx-faq__list\"\n hoverable\n :class=\"{ 'is-loading': loading }\"\n plain\n item-key=\"id\"\n :handle=\"sortable ? 'grip' : 'row'\"\n :item-label=\"(item: QuestionRow) => item.question\"\n :disabled=\"!sortable\"\n @move=\"reorder\"\n >\n <template #default=\"{ item }\">\n <component\n :is=\"inBin ? 'div' : 'button'\"\n :type=\"inBin ? undefined : 'button'\"\n class=\"wx-faq-row\"\n :class=\"{ 'is-current': selected === item.id }\"\n @click=\"inBin ? undefined : choose(item.id)\"\n >\n <span class=\"wx-faq-row__question\">{{ item.question }}</span>\n\n <span class=\"wx-faq-row__facts\">\n <wx-badge v-if=\"!item.published\" dot size=\"sm\">\n {{ t('question.not-published') }}\n </wx-badge>\n <wx-tooltip v-else-if=\"unseen(item)\" :content=\"t('question.seen-nowhere')\">\n <wx-badge type=\"warning\" dot size=\"sm\">\n {{ t('question.not-published') }}\n </wx-badge>\n </wx-tooltip>\n\n <wx-badge v-for=\"chip in item.categories\" :key=\"chip.id\" size=\"sm\" round>\n {{ chip.title }}\n </wx-badge>\n\n <span v-if=\"item.locales.length > 0\" class=\"wx-faq-row__locales\">\n {{ item.locales.join(' · ') }}\n </span>\n </span>\n </component>\n </template>\n\n <template #actions=\"{ item }\">\n <wx-row-menu :actions=\"actionsFor(item)\" :label=\"item.question\" />\n </template>\n </wx-sortable-list>\n\n <!-- What a drag does here, or why there is nothing to drag. -->\n <wx-alert\n v-if=\"canManage && !inBin\"\n class=\"wx-faq__note\"\n type=\"info\"\n :description=\"order.hint.value\"\n />\n </template>\n </template>\n\n <template #detail=\"{ inline, back }\">\n <question-pane\n v-if=\"selected !== null\"\n :id=\"selected === 'new' ? null : selected\"\n :key=\"String(selected)\"\n :category=\"category\"\n :inline=\"inline\"\n @back=\"back\"\n @saved=\"load\"\n @created=\"created\"\n @removed=\"removed\"\n />\n </template>\n\n <template #empty>\n <wx-empty\n :title=\"t('question.choose')\"\n :description=\"canManage ? t('question.choose-help') : undefined\"\n />\n </template>\n </wx-list-detail>\n </wx-card>\n </wx-list-screen>\n</template>\n\n<style>\n/*\n * What scrolls here is the page, the way it does on every other list of the panel. The card's\n * corners are the screen's, and the panes paint their own background to the edge: `clip`, not\n * `hidden`, which would make this a scroll container and pin everything sticky inside to a box\n * that never moves (CLAUDE.md §4).\n */\n.wx-faq > .wx-card__body {\n display: flex;\n flex-direction: column;\n overflow: clip;\n border-radius: inherit;\n}\n</style>\n\n<style scoped>\n.wx-faq__bar {\n display: flex;\n flex-wrap: wrap;\n gap: var(--wx-space-8);\n padding: var(--wx-space-12) var(--wx-space-12) var(--wx-space-8);\n}\n\n.wx-faq__search {\n flex: 1 1 180px;\n min-width: 0;\n}\n\n/* The select is a fragment (it carries its dropdown beside it), so our scope attribute never\n reaches it: styled from our own wrapper (CLAUDE.md §4). */\n.wx-faq__bar > :deep(.wx-select) {\n flex: 1 1 140px;\n min-width: 0;\n}\n\n.wx-faq__new {\n display: flex;\n align-items: center;\n gap: var(--wx-space-8);\n width: calc(100% - 2 * var(--wx-space-8));\n margin: 0 var(--wx-space-8) var(--wx-space-4);\n padding: var(--wx-space-8);\n border: 1px dashed var(--wx-border-default);\n border-radius: var(--wx-radius-sm);\n background: none;\n font: inherit;\n color: var(--wx-color-primary);\n text-align: start;\n cursor: pointer;\n}\n\n.wx-faq__new:hover,\n.wx-faq__new.is-current {\n background: var(--wx-bg-subtle);\n}\n\n.wx-faq__loading {\n padding: var(--wx-space-12);\n}\n\n.wx-faq__list {\n padding-inline: var(--wx-space-8);\n}\n\n.wx-faq__list.is-loading {\n opacity: 0.6;\n}\n\n.wx-faq__list :deep(.wx-sortable-list__row) {\n gap: var(--wx-space-4);\n padding-inline: var(--wx-space-4);\n}\n\n.wx-faq__note {\n margin: var(--wx-space-8);\n}\n\n.wx-faq-row {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: var(--wx-space-4);\n /* The row is the target: a short question should not leave half the line dead — and a\n button, unlike a link, is only as wide as what is in it until it is told otherwise. */\n flex: 1 1 auto;\n width: 100%;\n min-width: 0;\n padding-block: var(--wx-space-8);\n padding-inline: 0;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: start;\n}\n\nbutton.wx-faq-row {\n cursor: pointer;\n}\n\n.wx-faq-row.is-current .wx-faq-row__question {\n color: var(--wx-color-primary);\n}\n\n/* Two lines, then an ellipsis: a question is a sentence, and one line of it is rarely enough to\n tell two apart. */\n.wx-faq-row__question {\n display: -webkit-box;\n overflow: hidden;\n font-weight: var(--wx-font-weight-medium);\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow-wrap: anywhere;\n}\n\n.wx-faq-row__facts {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--wx-space-4);\n min-width: 0;\n}\n\n.wx-faq-row__facts:empty {\n display: none;\n}\n\n.wx-faq-row__locales {\n color: var(--wx-text-muted);\n font-family: var(--wx-font-family-mono);\n font-size: var(--wx-font-size-xs);\n text-transform: uppercase;\n}\n</style>\n","import { categoryRoutes, type AdminModule, type CategoriesOptions } from '@webx-ui/module-admin'\nimport AnchorField from './AnchorField.vue'\nimport QuestionsPage from './QuestionsPage.vue'\n\nexport interface FaqOptions {\n /** Where the questions live inside the panel. The categories sit under it. */\n path?: string\n}\n\n/**\n * The categories of the FAQ as the panel's shared category screens see them. Exported so that a\n * panel mounting the screens somewhere of its own does not have to repeat the words.\n *\n * No words about addresses: a category here has none (decision 2) — it is a button of the filter\n * on the site, and the shared list leaves the address line out when the server says so.\n */\nexport function faqCategoriesOptions(path = '/faq'): CategoriesOptions {\n return {\n api: 'faq/categories',\n path: `${path}/categories`,\n name: 'webx.faq.categories',\n module: 'faq-categories',\n screen: 'faq.category-form',\n manage: 'faq.categories.manage',\n count: 'questions_count',\n // The list narrowed to one category is also where its own order is dragged.\n items: (id) => ({ path, query: { category: String(id) } }),\n words: {\n new: 'webx-faq::category.new',\n empty: 'webx-faq::category.empty',\n 'empty-help': 'webx-faq::category.empty-help',\n order: 'webx-faq::category.order',\n hidden: 'webx-faq::category.hidden',\n count: 'webx-faq::category.questions',\n 'show-items': 'webx-faq::category.show-questions',\n 'delete-blocked': 'webx-faq::category.delete-blocked',\n 'delete-text': 'webx-faq::category.delete-text',\n deleted: 'webx-faq::category.deleted',\n saved: 'webx-faq::category.saved',\n 'field-title': 'webx-faq::category.field-title',\n },\n }\n}\n\n/**\n * The FAQ as sections of the panel: questions, and their categories (§4.5).\n *\n * Two modules rather than one, because the navigation is one entry per module; the server puts\n * both in the `faq` group, which is what draws them under one heading. A section whose server\n * half is not installed never appears — the entry is built from the manifest.\n */\nexport function faq(options: FaqOptions = {}): AdminModule[] {\n const path = options.path ?? '/faq'\n\n return [\n {\n id: 'faq',\n path,\n // One route: the open question is in the address (`?question=`), beside the list it was\n // opened from, so a link to it is a link to both.\n routes: [{ path, name: 'webx.faq', component: QuestionsPage, props: { base: path } }],\n /*\n * The one node of `faq.form` only this module can draw. A field in the screen's eyes — it\n * gets the label and the help of its node — but without a name, so nothing is bound.\n */\n types: {\n 'wx-faq-anchor': { component: AnchorField, kind: 'field' },\n },\n },\n {\n id: 'faq-categories',\n path: `${path}/categories`,\n routes: categoryRoutes(faqCategoriesOptions(path)),\n },\n ]\n}\n"],"names":["questionEditorKey","provideQuestionEditor","editor","provide","useQuestionEditor","inject","faqMessages","seeded","useFaqMessages","i18n","useAdmin","t","useTranslate","anchor","computed","fragment","copy","toast","_openBlock","_createElementBlock","_hoisted_1","_Fragment","_createVNode","_unref","WxText","WxButton","WxIcon","_createTextVNode","_createBlock","FAQ_API","createFaqApi","admin","base","data","body","query","search","id","values","ids","category","reorderItems","props","__props","emit","__emit","context","api","locales","useLocales","message","useErrorText","question","ref","snapshot","loading","saving","errors","canManage","current","dirty","state","title","written","fallback","localizedValue","take","detail","blank","load","error","save","made","remove","row","confirm","leaveGuard","event","onKeydown","useBodyKeys","onMounted","onBeforeUnmount","mayLeave","onBeforeRouteUpdate","to","from","onBeforeRouteLeave","actions","WxSkeleton","WxScreenHead","WxBadge","WxScreen","$event","WxActionBar","WxSaveState","route","useRoute","router","useRouter","rows","categories","module","view","value","inBin","raw","q","typed","selected","open","close","views","order","useItemOrder","sortable","emptyText","answer","watch","pause","narrow","choose","created","reorder","actionsFor","restore","copyLink","removed","unseen","WxListScreen","WxCard","WxListDetail","_createElementVNode","WxInput","WxSelect","item","_normalizeClass","WxEmpty","WxSortableList","_withCtx","_resolveDynamicComponent","_hoisted_2","_toDisplayString","_hoisted_3","WxTooltip","_renderList","chip","_hoisted_4","WxRowMenu","WxAlert","inline","back","QuestionPane","faqCategoriesOptions","path","faq","options","QuestionsPage","AnchorField","categoryRoutes"],"mappings":"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"}
|
package/dist/style.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.wx-faq-anchor[data-v-e6091b48]{display:flex;flex-wrap:wrap;align-items:center;gap:var(--wx-space-8);min-width:0}.wx-faq-anchor__value[data-v-e6091b48]{flex:1 1 auto;min-width:0}.wx-faq-question[data-v-fe493d3f]{display:flex;flex-direction:column;gap:var(--wx-gap, var(--wx-space-16));min-width:0;padding:var(--wx-gap, var(--wx-space-16));container-type:inline-size}.wx-faq-question__ghost[data-v-fe493d3f]{max-width:480px}.wx-faq>.wx-card__body{display:flex;flex-direction:column;overflow:clip;border-radius:inherit}.wx-faq__bar[data-v-
|
|
1
|
+
.wx-faq-anchor[data-v-e6091b48]{display:flex;flex-wrap:wrap;align-items:center;gap:var(--wx-space-8);min-width:0}.wx-faq-anchor__value[data-v-e6091b48]{flex:1 1 auto;min-width:0}.wx-faq-question[data-v-fe493d3f]{display:flex;flex-direction:column;gap:var(--wx-gap, var(--wx-space-16));min-width:0;padding:var(--wx-gap, var(--wx-space-16));container-type:inline-size}.wx-faq-question__ghost[data-v-fe493d3f]{max-width:480px}.wx-faq>.wx-card__body{display:flex;flex-direction:column;overflow:clip;border-radius:inherit}.wx-faq__bar[data-v-adc3e0e7]{display:flex;flex-wrap:wrap;gap:var(--wx-space-8);padding:var(--wx-space-12) var(--wx-space-12) var(--wx-space-8)}.wx-faq__search[data-v-adc3e0e7]{flex:1 1 180px;min-width:0}.wx-faq__bar[data-v-adc3e0e7]>.wx-select{flex:1 1 140px;min-width:0}.wx-faq__new[data-v-adc3e0e7]{display:flex;align-items:center;gap:var(--wx-space-8);width:calc(100% - 2 * var(--wx-space-8));margin:0 var(--wx-space-8) var(--wx-space-4);padding:var(--wx-space-8);border:1px dashed var(--wx-border-default);border-radius:var(--wx-radius-sm);background:none;font:inherit;color:var(--wx-color-primary);text-align:start;cursor:pointer}.wx-faq__new[data-v-adc3e0e7]:hover,.wx-faq__new.is-current[data-v-adc3e0e7]{background:var(--wx-bg-subtle)}.wx-faq__loading[data-v-adc3e0e7]{padding:var(--wx-space-12)}.wx-faq__list[data-v-adc3e0e7]{padding-inline:var(--wx-space-8)}.wx-faq__list.is-loading[data-v-adc3e0e7]{opacity:.6}.wx-faq__list[data-v-adc3e0e7] .wx-sortable-list__row{gap:var(--wx-space-4);padding-inline:var(--wx-space-4)}.wx-faq__note[data-v-adc3e0e7]{margin:var(--wx-space-8)}.wx-faq-row[data-v-adc3e0e7]{display:flex;flex-direction:column;align-items:stretch;gap:var(--wx-space-4);flex:1 1 auto;width:100%;min-width:0;padding-block:var(--wx-space-8);padding-inline:0;border:0;background:none;font:inherit;color:inherit;text-align:start}button.wx-faq-row[data-v-adc3e0e7]{cursor:pointer}.wx-faq-row.is-current .wx-faq-row__question[data-v-adc3e0e7]{color:var(--wx-color-primary)}.wx-faq-row__question[data-v-adc3e0e7]{display:-webkit-box;overflow:hidden;font-weight:var(--wx-font-weight-medium);-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow-wrap:anywhere}.wx-faq-row__facts[data-v-adc3e0e7]{display:flex;flex-wrap:wrap;align-items:center;gap:var(--wx-space-4);min-width:0}.wx-faq-row__facts[data-v-adc3e0e7]:empty{display:none}.wx-faq-row__locales[data-v-adc3e0e7]{color:var(--wx-text-muted);font-family:var(--wx-font-family-mono);font-size:var(--wx-font-size-xs);text-transform:uppercase}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webx-ui/module-faq",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "FAQ for the WebX UI admin panel: questions and answers with flat categories, ordered by hand, shown on the site by a block.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"vue-router": "^4.5.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@webx-ui/core": "^0.
|
|
47
|
-
"@webx-ui/module-admin": "^0.26.
|
|
48
|
-
"@webx-ui/schema": "^0.9.
|
|
46
|
+
"@webx-ui/core": "^0.41.0",
|
|
47
|
+
"@webx-ui/module-admin": "^0.26.1",
|
|
48
|
+
"@webx-ui/schema": "^0.9.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^24.10.1",
|