@webx-ui/module-faq 0.1.13 → 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 CHANGED
@@ -79,7 +79,7 @@ const Xe = {
79
79
  saved: "The category is saved."
80
80
  }
81
81
  }, ue = /* @__PURE__ */ new WeakSet();
82
- function ne() {
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
- ne();
94
+ oe();
95
95
  const s = te("webx-faq"), a = q(() => r?.question.value?.anchor ?? ""), c = q(() => a.value === "" ? "" : `#${a.value}`);
96
- async function n() {
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: n
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
- }), oe = (l, r) => {
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__ */ oe(Ze, [["__scopeId", "data-v-e6091b48"]]), Z = "faq/questions";
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(), n = he(c), B = Ve();
172
- ne();
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 o = g.value.question, h = s.id === null ? u("question.new") : x.value?.question ?? "";
175
- return Fe(o, B.active.value, h) || u("question.untitled");
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(o) {
179
- x.value = o.question, g.value = o.values, N.value = JSON.stringify(o.values);
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 o = {
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 = o, N.value = JSON.stringify(o);
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 n.get(s.id));
195
- } catch (o) {
196
- w.danger(Q(o)), a("back");
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 o = s.id === null, h = o ? await n.create(g.value) : await n.save(s.id, g.value);
207
- F(h), w.success(u("question.saved")), a(o ? "created" : "saved", h.question);
208
- } catch (o) {
209
- const h = o.body;
210
- h?.errors ? (T.value = h.errors, w.danger(u("question.save-failed"))) : w.danger(Q(o));
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 o = x.value;
218
- if (!(!o || !await Y({
219
- title: u("question.delete-title", { title: o.question }),
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 n.remove(o.id), w.success(u("question.deleted")), N.value = V.value, a("removed", o.id);
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(o) {
232
- m.value && o.preventDefault();
231
+ function z(n) {
232
+ m.value && n.preventDefault();
233
233
  }
234
- function D(o) {
235
- (o.ctrlKey || o.metaKey) && o.key.toLowerCase() === "s" && (o.preventDefault(), (m.value || s.id === null) && E());
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 (o, h) => o.query.question === h.query.question ? !0 : await I()
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 (o, h) => (v(), A("div", {
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__ */ oe(tt, [["__scopeId", "data-v-fe493d3f"]]), nt = { class: "wx-faq__bar" }, ot = { class: "wx-faq-row__question" }, st = { class: "wx-faq-row__facts" }, it = {
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
- ne();
340
- const n = 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 ?? n("module.questions")
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: n("question.all") },
364
- { value: "trashed", label: n("question.bin"), icon: "trash" }
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 !== "" ? n("question.empty-search") : f.value ? n("question.empty-bin") : n("question.empty"));
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 o(e) {
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, n("question.reorder-failed"))), await W();
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: n("question.restore"),
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: n("question.copy-link"),
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: n("question.delete"),
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: n("question.delete-title", { title: e.question }),
452
- message: n("question.delete-text"),
453
- confirmText: n("question.delete"),
454
- cancelText: n("question.cancel"),
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(n("question.deleted")), se(e.id);
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(n("question.restored"));
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(n("question.link-copied"));
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(n)("module.questions")
503
+ "detail-label": t(o)("module.questions")
504
504
  }, {
505
505
  list: d(() => [
506
- j("div", nt, [
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(n)("question.search"),
514
- "aria-label": t(n)("question.search")
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(n)("question.any-category"),
529
- "aria-label": t(n)("question.filter-category"),
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(n)("question.new")), 1)
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(n)("question.empty-help") : void 0
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", ot, _(i.question), 1),
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(n)("question.seen-nowhere")
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(n)("question.not-published")), 1)
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(n)("question.not-published")), 1)
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: o,
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(n)("question.choose"),
650
- description: g.value ? t(n)("question.choose-help") : void 0
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__ */ oe(lt, [["__scopeId", "data-v-6b3bda78"]]);
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-6b3bda78]{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-6b3bda78]{flex:1 1 180px;min-width:0}.wx-faq__bar[data-v-6b3bda78]>.wx-select{flex:1 1 140px;min-width:0}.wx-faq__new[data-v-6b3bda78]{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-6b3bda78]:hover,.wx-faq__new.is-current[data-v-6b3bda78]{background:var(--wx-bg-subtle)}.wx-faq__loading[data-v-6b3bda78]{padding:var(--wx-space-12)}.wx-faq__list[data-v-6b3bda78]{padding-inline:var(--wx-space-8)}.wx-faq__list.is-loading[data-v-6b3bda78]{opacity:.6}.wx-faq__list[data-v-6b3bda78] .wx-sortable-list__row{gap:var(--wx-space-4);padding-inline:var(--wx-space-4);border-radius:var(--wx-radius-sm)}.wx-faq__list[data-v-6b3bda78] .wx-sortable-list__row:hover,.wx-faq__list[data-v-6b3bda78] .wx-sortable-list__row:has(.is-current){background:var(--wx-bg-subtle)}.wx-faq__note[data-v-6b3bda78]{margin:var(--wx-space-8)}.wx-faq-row[data-v-6b3bda78]{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-6b3bda78]{cursor:pointer}.wx-faq-row.is-current .wx-faq-row__question[data-v-6b3bda78]{color:var(--wx-color-primary)}.wx-faq-row__question[data-v-6b3bda78]{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-6b3bda78]{display:flex;flex-wrap:wrap;align-items:center;gap:var(--wx-space-4);min-width:0}.wx-faq-row__facts[data-v-6b3bda78]:empty{display:none}.wx-faq-row__locales[data-v-6b3bda78]{color:var(--wx-text-muted);font-family:var(--wx-font-family-mono);font-size:var(--wx-font-size-xs);text-transform:uppercase}
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.13",
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.40.0",
47
- "@webx-ui/module-admin": "^0.25.0",
48
- "@webx-ui/schema": "^0.9.0"
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",