@webx-ui/module-press 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 webx-ui
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # @webx-ui/module-press
2
+
3
+ The front end of the press section of the [WebX UI](https://github.com/webx-ui/webx-ui) admin
4
+ panel: the **outlets** that wrote about a site — a magazine, a paper, a portal — and the
5
+ **articles** in them, each leading to its address or to a PDF from the library.
6
+
7
+ The other half is the Composer package `webx-ui/module-press`, which owns the outlets, their
8
+ pages on the site, the three offered blocks, `press()` for templates and the API. The section
9
+ appears in the panel when both halves are installed.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @webx-ui/module-press
15
+ ```
16
+
17
+ ```ts
18
+ import { createAdmin } from '@webx-ui/module-admin'
19
+ import { press } from '@webx-ui/module-press'
20
+ import '@webx-ui/module-press/style.css'
21
+
22
+ createAdmin({
23
+ basePath: '/cms',
24
+ modules: [press()],
25
+ }).mount()
26
+ ```
27
+
28
+ `press()` is one section and one entry of the menu, without a group of its own.
29
+ `press({ path: '/media-coverage' })` puts it somewhere else inside the panel.
30
+
31
+ On the server, the block types the module offers are installed once:
32
+
33
+ ```bash
34
+ php artisan webx:blocks:offered --install --module=press
35
+ ```
36
+
37
+ ## Outlets
38
+
39
+ A list and the form of one outlet side by side, without pages: the list is where outlets are put
40
+ in order — the order of the strip of logos and the catalogue on the site — and a drag cannot cross
41
+ a page boundary. The open outlet is in the address (`?outlet=12`). **New outlet** is a row that
42
+ opens an empty form; the outlet is created by its first save. On a phone the form slides over the
43
+ list and draws its own «Back». While a search narrows the list, the grips disappear.
44
+
45
+ A row shows the logo (or the initials), the name, a star when the outlet is in the strip of logos,
46
+ how many articles it has and the languages a reader sees it in. A published outlet seen in no
47
+ language — none of its articles has a title yet, or all of them are hidden — is marked.
48
+
49
+ The form is the `press.outlet-form` screen, described in JSON on the server, in three tabs:
50
+
51
+ - **General** — the logo, the name, the address, the website, a few words about the outlet,
52
+ **Published**, **In the strip of logos** and a card for the project's own fields;
53
+ - **Articles** — the articles as cards in their order: the title and the excerpt in every
54
+ language, the kind, the date and how much of it is known, the link, the PDF, and **Do not
55
+ show**. A collapsed card reads «#2 · the title». Drag a card by its grip to reorder;
56
+ - **SEO** — the card of the outlet's page, when `module-seo` is installed.
57
+
58
+ The outlet and its articles are saved together, with the button or `Ctrl+S`; leaving with unsaved
59
+ changes asks first. A refused article opens by itself with the error under its field. An outlet has
60
+ no draft: a save is on the site.
61
+
62
+ ## On the site
63
+
64
+ Every outlet has a page under a prefix (`/press/tatler-asia`); the prefix itself is a page of
65
+ `module-pages` with the catalogue block on it. The blocks — a strip of logos, the catalogue, the
66
+ latest articles — and `press()` for templates are described in the
67
+ [guide](https://webx-ui.github.io/webx-ui/guide/press.html).
68
+
69
+ ## Licence
70
+
71
+ MIT
@@ -0,0 +1,21 @@
1
+ import { OutletSummary } from './types';
2
+ type __VLS_Props = {
3
+ /** `null` — an outlet not written yet. */
4
+ id: number | null;
5
+ /** False in the drawer a narrow screen opens this in. */
6
+ inline?: boolean;
7
+ };
8
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
9
+ saved: (outlet: OutletSummary) => any;
10
+ back: () => any;
11
+ created: (outlet: OutletSummary) => any;
12
+ removed: (id: number) => any;
13
+ }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
14
+ onSaved?: ((outlet: OutletSummary) => any) | undefined;
15
+ onBack?: (() => any) | undefined;
16
+ onCreated?: ((outlet: OutletSummary) => any) | undefined;
17
+ onRemoved?: ((id: number) => any) | undefined;
18
+ }>, {
19
+ inline: boolean;
20
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
21
+ export default _default;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The outlets: all of them on the left, the one being written on the right (§4.9, decision 15).
3
+ *
4
+ * The shape of the reviews, for the same reason: an outlet is typed in from a clipping in a
5
+ * minute, and the next one is usually under it. On a phone the form is the whole screen, in the
6
+ * drawer `WxListDetail` raises, and draws its own way back.
7
+ *
8
+ * One order, the general one (decision 6): a drag moves it; a search or the bin shows a
9
+ * selection, so the grips go away and the line under the list says why. The order of the
10
+ * articles is not here — it is the order of the rows in the outlet's form.
11
+ *
12
+ * Everything the screen is looking at lives in the address — the view, the search and the open
13
+ * outlet — so a link to an outlet is a link to it in its list.
14
+ */
15
+ type __VLS_Props = {
16
+ base?: string;
17
+ };
18
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
19
+ base: string;
20
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
21
+ export default _default;
package/dist/api.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { AdminContext } from '@webx-ui/module-admin';
2
+ import { ScreenModel } from '@webx-ui/schema';
3
+ import { OutletDetail, OutletQuery, OutletRow, OutletsList } from './types';
4
+ export interface PressApi {
5
+ list(query?: OutletQuery): Promise<OutletsList>;
6
+ get(id: number): Promise<OutletDetail>;
7
+ /**
8
+ * A new outlet out of the values of its form, articles included, in one request: the form is
9
+ * filled in first and the record made on the first save, so nobody leaves an empty outlet behind.
10
+ */
11
+ create(values: ScreenModel): Promise<OutletDetail>;
12
+ /** Refused with a 422 under the name of the field — `articles.<n>.<field>` for an article. */
13
+ save(id: number, values: ScreenModel): Promise<OutletDetail>;
14
+ remove(id: number): Promise<void>;
15
+ restore(id: number): Promise<OutletRow>;
16
+ /** The order of the whole list. The order of the articles is the order of the form's rows. */
17
+ reorder(ids: number[]): Promise<void>;
18
+ }
19
+ /** The path of the section under the panel's API, and of its order. */
20
+ export declare const PRESS_API = "press";
21
+ /** Everything under `/press`, below the panel's API path (§4.10). */
22
+ export declare function createPressApi(admin: AdminContext): PressApi;
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Puts this package's English under the panel's dictionary, once per panel. */
2
+ export declare function usePressMessages(): void;
@@ -0,0 +1,6 @@
1
+ export { press, type PressOptions } from './module';
2
+ export { createPressApi, PRESS_API, type PressApi } from './api';
3
+ export { pressMessages } from './messages';
4
+ export { default as WxPressPage } from './PressPage';
5
+ export { default as WxOutletPane } from './OutletPane';
6
+ export type { ArticleValues, OutletDetail, OutletQuery, OutletRow, OutletsList, OutletSummary, } from './types';
package/dist/index.js ADDED
@@ -0,0 +1,657 @@
1
+ import { defineComponent as se, ref as W, computed as h, onMounted as he, onBeforeUnmount as ne, openBlock as v, createElementBlock as D, createBlock as b, unref as t, Fragment as re, createVNode as w, createSlots as fe, withCtx as p, createTextVNode as z, toDisplayString as $, createCommentVNode as O, watch as te, createElementVNode as N, normalizeClass as H, resolveDynamicComponent as me } from "vue";
2
+ import { onBeforeRouteUpdate as we, onBeforeRouteLeave as ye, useRoute as ge, useRouter as be } from "vue-router";
3
+ import { useLocales as _e, localizedValue as xe, WxSkeleton as ie, WxBadge as Q, WxActionBar as ke, WxButton as Se, toast as _, confirm as X, WxCard as We, WxListDetail as Te, WxEmpty as le, WxInput as Pe, WxIcon as Y, WxSortableList as Ae, WxAvatar as qe, WxTooltip as ae, WxAlert as Be } from "@webx-ui/core";
4
+ import { reorderItems as $e, useAdmin as ee, useTranslate as ue, useErrorText as ce, provideRecordAddress as Le, WxScreenHead as Ne, WxScreen as Oe, WxSaveState as Ce, useItemOrder as Ve, WxListScreen as De, WxRowMenu as Ie } from "@webx-ui/module-admin";
5
+ const Z = "press";
6
+ function de(u) {
7
+ const d = `${u.apiPath}/${Z}`, i = (s) => s.data;
8
+ return {
9
+ list: (s = {}) => {
10
+ const f = new URLSearchParams();
11
+ return s.search && f.set("search", s.search), s.trashed && f.set("trashed", "1"), u.http.get(f.size > 0 ? `${d}?${f}` : d);
12
+ },
13
+ get: (s) => u.http.get(`${d}/${s}`).then(i),
14
+ create: (s) => u.http.post(d, { values: s }).then(i),
15
+ save: (s, f) => u.http.put(`${d}/${s}`, { values: f }).then(i),
16
+ remove: (s) => u.http.delete(`${d}/${s}`).then(() => {
17
+ }),
18
+ // The resource of the list line, and not the form's answer: a restored outlet stays closed.
19
+ restore: (s) => u.http.post(`${d}/${s}/restore`, {}).then(i),
20
+ reorder: (s) => $e(u, Z, s, null)
21
+ };
22
+ }
23
+ const Re = {
24
+ module: {
25
+ press: "Press"
26
+ },
27
+ outlet: {
28
+ new: "New outlet",
29
+ untitled: "Untitled",
30
+ search: "Search the outlets",
31
+ empty: "No outlets yet.",
32
+ "empty-help": "An outlet is a magazine, a paper or a site that wrote about you; its articles are added on its form.",
33
+ "empty-search": "Nothing matches that.",
34
+ "empty-bin": "The bin is empty.",
35
+ all: "All",
36
+ trashed: "Bin",
37
+ "not-published": "Not published",
38
+ "visible-nowhere": "Published, but seen in no language: none of its articles has a title yet.",
39
+ "articles-count": "Articles: :count",
40
+ "no-articles": "No articles yet",
41
+ featured: "In the logo strip",
42
+ "seen-in": "Seen in: :locales",
43
+ choose: "Choose an outlet",
44
+ "choose-help": "Or add a new one: the first line of the list.",
45
+ save: "Save",
46
+ saved: "The outlet is saved.",
47
+ "save-failed": "The outlet was not saved.",
48
+ cancel: "Cancel",
49
+ delete: "Delete",
50
+ "delete-title": "Delete “:name”?",
51
+ "delete-text": "It goes to the bin with its articles and leaves the site and every block. Restored, it comes back with them.",
52
+ deleted: "The outlet is in the bin.",
53
+ restore: "Restore",
54
+ restored: "The outlet is back.",
55
+ "reorder-failed": "The new order was not saved.",
56
+ "leave-title": "Leave without saving?",
57
+ "leave-text": "What you wrote in this outlet is not on the server.",
58
+ leave: "Leave",
59
+ back: "Back to the list",
60
+ open: "Open on the site",
61
+ "address-moving": "The page of the outlet moves to the new address when you save."
62
+ },
63
+ screen: {
64
+ general: "General",
65
+ outlet: "Outlet",
66
+ logo: "Logo",
67
+ "logo-help": "Without a logo the site prints the name.",
68
+ title: "Name",
69
+ "title-help": "Where it is not written in a language, the name from another one is shown.",
70
+ slug: "Address",
71
+ "slug-help": "The outlet's page. A language with articles and no address of its own takes this one.",
72
+ "website-url": "Website",
73
+ "website-url-help": "An address that starts with http:// or https://.",
74
+ summary: "About the outlet",
75
+ "summary-help": "A few words under the name on its page, and its description for search engines.",
76
+ settings: "Settings",
77
+ published: "Published",
78
+ "published-help": "On the site at once — in the languages it has articles in.",
79
+ featured: "In the strip of logos",
80
+ "featured-help": "Shown by the logos block set to the marked outlets only.",
81
+ articles: "Articles",
82
+ "articles-help": "In the order they are shown on the outlet's page. Saved with the outlet.",
83
+ "article-label": "#:number · :title",
84
+ "article-untitled": "Untitled",
85
+ "article-title": "Title",
86
+ "article-title-help": "An article is shown in a language only when it has a title in it.",
87
+ excerpt: "Excerpt",
88
+ kind: "Kind",
89
+ "kind-none": "No kind",
90
+ "published-on": "Date",
91
+ "date-precision": "Known to",
92
+ "date-precision-help": "How much of the date is printed: the day, the month, or the year only.",
93
+ precision: {
94
+ day: "Day",
95
+ month: "Month",
96
+ year: "Year"
97
+ },
98
+ url: "Link",
99
+ "url-help": "The article on the outlet's site. Without it the title leads to the PDF.",
100
+ file: "PDF",
101
+ "file-help": "A PDF from the library — a scan of the page, say.",
102
+ hidden: "Do not show",
103
+ "hidden-help": "Kept here, and shown nowhere on the site.",
104
+ seo: "SEO",
105
+ "seo-empty": "The SEO card appears here when module-seo is installed."
106
+ },
107
+ kinds: {
108
+ mention: "Mention",
109
+ interview: "Interview",
110
+ expert_comment: "Expert comment",
111
+ authored: "Authored article"
112
+ }
113
+ }, oe = /* @__PURE__ */ new WeakSet();
114
+ function ve() {
115
+ try {
116
+ const { i18n: u } = ee();
117
+ oe.has(u) || (u.defaults("webx-press", Re), oe.add(u));
118
+ } catch {
119
+ }
120
+ }
121
+ const ze = /* @__PURE__ */ se({
122
+ name: "WxOutletPane",
123
+ __name: "OutletPane",
124
+ props: {
125
+ id: {},
126
+ inline: { type: Boolean, default: !0 }
127
+ },
128
+ emits: ["back", "saved", "created", "removed"],
129
+ setup(u, { emit: d }) {
130
+ const i = u, s = d, f = ee(), a = de(f), A = _e();
131
+ ve();
132
+ const n = ue("webx-press"), q = ce(), y = W(null), x = W({}), L = W(null), m = W(""), k = W(i.id !== null), S = W(!1), g = W({}), B = h(() => f.can("press.manage")), E = h(() => JSON.stringify(x.value)), T = h(() => m.value !== "" && E.value !== m.value), U = h(() => S.value ? "saving" : T.value || i.id === null ? "unsaved" : "saved"), j = h(() => {
133
+ const l = x.value.title, r = i.id === null ? n("outlet.new") : y.value?.title ?? "";
134
+ return xe(l, A.active.value, r) || n("outlet.untitled");
135
+ });
136
+ Le({
137
+ values: x,
138
+ prefix: L,
139
+ path: h(() => P(y.value?.url ?? null, A.active.value)),
140
+ moving: () => n("outlet.address-moving")
141
+ });
142
+ function P(l, r) {
143
+ if (l === null) return null;
144
+ let e;
145
+ try {
146
+ e = new URL(l, "http://site.invalid").pathname.split("/").filter((V) => V !== "");
147
+ } catch {
148
+ return null;
149
+ }
150
+ const c = A.list.value.map((V) => V.code), o = c.includes(e[0] ?? "") && e[0] !== c[0] ? e.shift() : c[0];
151
+ return o === void 0 || o === r ? e.join("/") : void 0;
152
+ }
153
+ function C(l) {
154
+ y.value = l.outlet, x.value = l.values, L.value = l.prefix, m.value = JSON.stringify(l.values);
155
+ }
156
+ if (i.id === null) {
157
+ const l = {
158
+ title: {},
159
+ slug: {},
160
+ summary: {},
161
+ published: !1,
162
+ featured: !1,
163
+ articles: []
164
+ };
165
+ x.value = l, m.value = JSON.stringify(l);
166
+ }
167
+ async function I() {
168
+ if (i.id !== null) {
169
+ k.value = !0;
170
+ try {
171
+ C(await a.get(i.id));
172
+ } catch (l) {
173
+ _.danger(q(l)), s("back");
174
+ } finally {
175
+ k.value = !1;
176
+ }
177
+ }
178
+ }
179
+ async function R() {
180
+ if (!(S.value || !B.value)) {
181
+ S.value = !0, g.value = {};
182
+ try {
183
+ const l = i.id === null, r = l ? await a.create(x.value) : await a.save(i.id, x.value);
184
+ C(r), _.success(n("outlet.saved")), s(l ? "created" : "saved", r.outlet);
185
+ } catch (l) {
186
+ const r = l.body;
187
+ r?.errors ? (g.value = r.errors, _.danger(n("outlet.save-failed"))) : _.danger(q(l));
188
+ } finally {
189
+ S.value = !1;
190
+ }
191
+ }
192
+ }
193
+ async function J() {
194
+ const l = y.value;
195
+ if (!(!l || !await X({
196
+ title: n("outlet.delete-title", { name: l.title }),
197
+ message: n("outlet.delete-text"),
198
+ confirmText: n("outlet.delete"),
199
+ cancelText: n("outlet.cancel"),
200
+ tone: "danger"
201
+ })))
202
+ try {
203
+ await a.remove(l.id), _.success(n("outlet.deleted")), m.value = E.value, s("removed", l.id);
204
+ } catch (e) {
205
+ _.danger(q(e));
206
+ }
207
+ }
208
+ function M(l) {
209
+ T.value && l.preventDefault();
210
+ }
211
+ function G(l) {
212
+ (l.ctrlKey || l.metaKey) && l.key.toLowerCase() === "s" && (l.preventDefault(), (T.value || i.id === null) && R());
213
+ }
214
+ he(() => {
215
+ I(), window.addEventListener("beforeunload", M);
216
+ }), ne(() => {
217
+ window.removeEventListener("beforeunload", M);
218
+ });
219
+ async function K() {
220
+ return !T.value || !B.value ? !0 : await X({
221
+ title: n("outlet.leave-title"),
222
+ message: n("outlet.leave-text"),
223
+ confirmText: n("outlet.leave"),
224
+ cancelText: n("outlet.cancel"),
225
+ tone: "danger"
226
+ });
227
+ }
228
+ we(
229
+ async (l, r) => l.query.outlet === r.query.outlet ? !0 : await K()
230
+ ), ye(K);
231
+ const F = h(() => {
232
+ const l = y.value, r = [];
233
+ if (l?.url) {
234
+ const e = l.url;
235
+ r.push({
236
+ key: "open",
237
+ label: n("outlet.open"),
238
+ icon: "external-link",
239
+ run: () => {
240
+ window.open(e, "_blank", "noopener");
241
+ }
242
+ });
243
+ }
244
+ return B.value && l !== null && r.push({
245
+ key: "delete",
246
+ label: n("outlet.delete"),
247
+ icon: "trash",
248
+ danger: !0,
249
+ menu: !0,
250
+ run: () => {
251
+ J();
252
+ }
253
+ }), r;
254
+ });
255
+ return (l, r) => (v(), D("div", {
256
+ class: "wx-outlet",
257
+ onKeydown: G
258
+ }, [
259
+ k.value ? (v(), b(t(ie), {
260
+ key: 0,
261
+ class: "wx-outlet__ghost",
262
+ title: "",
263
+ rows: 6
264
+ })) : (v(), D(re, { key: 1 }, [
265
+ w(t(Ne), {
266
+ level: 3,
267
+ back: !i.inline,
268
+ "back-label": t(n)("outlet.back"),
269
+ title: j.value,
270
+ actions: F.value,
271
+ "collapse-below": 480,
272
+ onBack: r[0] || (r[0] = (e) => s("back"))
273
+ }, fe({ _: 2 }, [
274
+ y.value && !y.value.published ? {
275
+ name: "title-after",
276
+ fn: p(() => [
277
+ w(t(Q), { dot: "" }, {
278
+ default: p(() => [
279
+ z($(t(n)("outlet.not-published")), 1)
280
+ ]),
281
+ _: 1
282
+ })
283
+ ]),
284
+ key: "0"
285
+ } : void 0
286
+ ]), 1032, ["back", "back-label", "title", "actions"]),
287
+ w(t(Oe), {
288
+ modelValue: x.value,
289
+ "onUpdate:modelValue": r[1] || (r[1] = (e) => x.value = e),
290
+ name: "press.outlet-form",
291
+ errors: g.value,
292
+ disabled: !B.value || S.value
293
+ }, null, 8, ["modelValue", "errors", "disabled"]),
294
+ B.value ? (v(), b(t(ke), { key: 0 }, {
295
+ state: p(() => [
296
+ w(t(Ce), { state: U.value }, null, 8, ["state"])
297
+ ]),
298
+ default: p(() => [
299
+ w(t(Se), {
300
+ type: "primary",
301
+ loading: S.value,
302
+ disabled: !T.value && i.id !== null,
303
+ onClick: R
304
+ }, {
305
+ default: p(() => [
306
+ z($(t(n)("outlet.save")), 1)
307
+ ]),
308
+ _: 1
309
+ }, 8, ["loading", "disabled"])
310
+ ]),
311
+ _: 1
312
+ })) : O("", !0)
313
+ ], 64))
314
+ ], 32));
315
+ }
316
+ }), pe = (u, d) => {
317
+ const i = u.__vccOpts || u;
318
+ for (const [s, f] of d)
319
+ i[s] = f;
320
+ return i;
321
+ }, Ee = /* @__PURE__ */ pe(ze, [["__scopeId", "data-v-396f08f3"]]), Ue = { class: "wx-press__bar" }, Me = {
322
+ key: 0,
323
+ class: "wx-outlet-row__logo"
324
+ }, Ke = ["src"], Fe = { class: "wx-outlet-row__body" }, je = { class: "wx-outlet-row__head" }, Je = { class: "wx-outlet-row__title" }, Ge = { class: "wx-outlet-row__facts" }, He = { class: "wx-outlet-row__count" }, Ye = ["aria-label"], Qe = /* @__PURE__ */ se({
325
+ __name: "PressPage",
326
+ props: {
327
+ base: { default: "/press" }
328
+ },
329
+ setup(u) {
330
+ const d = ee(), i = de(d), s = ge(), f = be();
331
+ ve();
332
+ const a = ue("webx-press"), A = ce(), n = W([]), q = W(!0), y = h(() => d.can("press.manage")), x = h(
333
+ () => d.state.manifest?.modules.find((e) => e.id === "press")?.title ?? a("module.press")
334
+ ), L = h({
335
+ get: () => s.query.view === "trashed" ? "trashed" : "",
336
+ set: (e) => {
337
+ const c = {
338
+ ...s.query,
339
+ view: e === "" ? void 0 : String(e)
340
+ };
341
+ delete c.outlet, f.replace({ query: c });
342
+ }
343
+ }), m = h(() => L.value === "trashed"), k = h(() => typeof s.query.q == "string" ? s.query.q : ""), S = W(k.value), g = h(() => {
344
+ const e = s.query.outlet;
345
+ return e === "new" ? y.value ? "new" : null : typeof e == "string" && e !== "" && Number(e) || null;
346
+ }), B = h({
347
+ get: () => g.value !== null,
348
+ set: (e) => {
349
+ e || R();
350
+ }
351
+ }), E = h(() => [
352
+ { value: "", label: a("outlet.all") },
353
+ { value: "trashed", label: a("outlet.trashed"), icon: "trash" }
354
+ ]), T = Ve(Z, () => ({ q: k.value, filtered: m.value })), U = h(() => y.value && T.sortable.value), j = h(() => k.value !== "" ? a("outlet.empty-search") : m.value ? a("outlet.empty-bin") : a("outlet.empty"));
355
+ async function P() {
356
+ q.value = !0;
357
+ try {
358
+ const e = await i.list({ search: k.value, trashed: m.value });
359
+ n.value = e.data;
360
+ } catch (e) {
361
+ _.danger(A(e));
362
+ } finally {
363
+ q.value = !1;
364
+ }
365
+ }
366
+ te(
367
+ () => [L.value, k.value],
368
+ () => {
369
+ P();
370
+ }
371
+ ), P();
372
+ let C;
373
+ te(S, (e) => {
374
+ clearTimeout(C), C = setTimeout(() => {
375
+ f.replace({ query: { ...s.query, q: e.trim() === "" ? void 0 : e } });
376
+ }, 300);
377
+ }), ne(() => clearTimeout(C));
378
+ function I(e) {
379
+ f.replace({ query: { ...s.query, outlet: String(e) } });
380
+ }
381
+ function R() {
382
+ const e = { ...s.query };
383
+ delete e.outlet, f.replace({ query: e });
384
+ }
385
+ function J(e) {
386
+ I(e.id), P();
387
+ }
388
+ async function M() {
389
+ try {
390
+ await T.move(n.value.map((e) => e.id));
391
+ } catch (e) {
392
+ _.danger(A(e, a("outlet.reorder-failed"))), await P();
393
+ }
394
+ }
395
+ function G(e) {
396
+ return y.value ? m.value ? [
397
+ {
398
+ key: "restore",
399
+ icon: "refresh",
400
+ label: a("outlet.restore"),
401
+ run: () => {
402
+ l(e);
403
+ }
404
+ }
405
+ ] : [
406
+ {
407
+ key: "delete",
408
+ icon: "trash",
409
+ label: a("outlet.delete"),
410
+ danger: !0,
411
+ run: () => {
412
+ K(e);
413
+ }
414
+ }
415
+ ] : [];
416
+ }
417
+ async function K(e) {
418
+ if (await X({
419
+ title: a("outlet.delete-title", { name: e.title }),
420
+ message: a("outlet.delete-text"),
421
+ confirmText: a("outlet.delete"),
422
+ cancelText: a("outlet.cancel"),
423
+ tone: "danger"
424
+ }))
425
+ try {
426
+ await i.remove(e.id), _.success(a("outlet.deleted")), F(e.id);
427
+ } catch (o) {
428
+ _.danger(A(o));
429
+ }
430
+ }
431
+ function F(e) {
432
+ g.value === e && R(), P();
433
+ }
434
+ async function l(e) {
435
+ try {
436
+ await i.restore(e.id), _.success(a("outlet.restored"));
437
+ } catch (c) {
438
+ _.danger(A(c));
439
+ } finally {
440
+ await P();
441
+ }
442
+ }
443
+ function r(e) {
444
+ return e.published && e.locales.length === 0;
445
+ }
446
+ return (e, c) => (v(), b(t(De), {
447
+ view: L.value,
448
+ "onUpdate:view": c[4] || (c[4] = (o) => L.value = o),
449
+ title: x.value,
450
+ views: E.value,
451
+ card: !1
452
+ }, {
453
+ default: p(() => [
454
+ w(t(We), {
455
+ class: "wx-press",
456
+ padding: "none"
457
+ }, {
458
+ default: p(() => [
459
+ w(t(Te), {
460
+ open: B.value,
461
+ "onUpdate:open": c[3] || (c[3] = (o) => B.value = o),
462
+ class: "wx-press__panes",
463
+ "list-width": 360,
464
+ "detail-min": 520,
465
+ "detail-label": t(a)("module.press")
466
+ }, {
467
+ list: p(() => [
468
+ N("div", Ue, [
469
+ w(t(Pe), {
470
+ modelValue: S.value,
471
+ "onUpdate:modelValue": c[0] || (c[0] = (o) => S.value = o),
472
+ class: "wx-press__search",
473
+ size: "sm",
474
+ clearable: "",
475
+ placeholder: t(a)("outlet.search"),
476
+ "aria-label": t(a)("outlet.search")
477
+ }, {
478
+ prefix: p(() => [
479
+ w(t(Y), {
480
+ name: "search",
481
+ size: "sm"
482
+ })
483
+ ]),
484
+ _: 1
485
+ }, 8, ["modelValue", "placeholder", "aria-label"])
486
+ ]),
487
+ y.value && !m.value ? (v(), D("button", {
488
+ key: 0,
489
+ type: "button",
490
+ class: H(["wx-press__new", { "is-current": g.value === "new" }]),
491
+ onClick: c[1] || (c[1] = (o) => I("new"))
492
+ }, [
493
+ w(t(Y), {
494
+ name: "plus",
495
+ size: "sm"
496
+ }),
497
+ z(" " + $(t(a)("outlet.new")), 1)
498
+ ], 2)) : O("", !0),
499
+ q.value && n.value.length === 0 ? (v(), b(t(ie), {
500
+ key: 1,
501
+ class: "wx-press__loading",
502
+ rows: 5
503
+ })) : n.value.length === 0 ? (v(), b(t(le), {
504
+ key: 2,
505
+ title: j.value,
506
+ description: !m.value && k.value === "" ? t(a)("outlet.empty-help") : void 0
507
+ }, null, 8, ["title", "description"])) : (v(), D(re, { key: 3 }, [
508
+ w(t(Ae), {
509
+ modelValue: n.value,
510
+ "onUpdate:modelValue": c[2] || (c[2] = (o) => n.value = o),
511
+ class: H(["wx-press__list", { "is-loading": q.value }]),
512
+ plain: "",
513
+ "item-key": "id",
514
+ handle: U.value ? "grip" : "row",
515
+ "item-label": (o) => o.title,
516
+ disabled: !U.value,
517
+ onMove: M
518
+ }, {
519
+ default: p(({ item: o }) => [
520
+ (v(), b(me(m.value ? "div" : "button"), {
521
+ type: m.value ? void 0 : "button",
522
+ class: H(["wx-outlet-row", { "is-current": g.value === o.id }]),
523
+ onClick: (V) => m.value ? void 0 : I(o.id)
524
+ }, {
525
+ default: p(() => [
526
+ o.logo?.thumb ? (v(), D("span", Me, [
527
+ N("img", {
528
+ src: o.logo.thumb,
529
+ alt: ""
530
+ }, null, 8, Ke)
531
+ ])) : (v(), b(t(qe), {
532
+ key: 1,
533
+ class: "wx-outlet-row__initials",
534
+ shape: "square",
535
+ name: o.title,
536
+ size: "md"
537
+ }, null, 8, ["name"])),
538
+ N("span", Fe, [
539
+ N("span", je, [
540
+ N("span", Je, $(o.title), 1),
541
+ o.featured ? (v(), b(t(ae), {
542
+ key: 0,
543
+ content: t(a)("outlet.featured")
544
+ }, {
545
+ default: p(() => [
546
+ w(t(Y), {
547
+ class: "wx-outlet-row__featured",
548
+ name: "star",
549
+ size: "sm",
550
+ "aria-label": t(a)("outlet.featured")
551
+ }, null, 8, ["aria-label"])
552
+ ]),
553
+ _: 1
554
+ }, 8, ["content"])) : O("", !0)
555
+ ]),
556
+ N("span", Ge, [
557
+ o.published ? r(o) ? (v(), b(t(ae), {
558
+ key: 1,
559
+ content: t(a)("outlet.visible-nowhere")
560
+ }, {
561
+ default: p(() => [
562
+ w(t(Q), {
563
+ type: "warning",
564
+ dot: "",
565
+ size: "sm"
566
+ }, {
567
+ default: p(() => [
568
+ z($(t(a)("outlet.not-published")), 1)
569
+ ]),
570
+ _: 1
571
+ })
572
+ ]),
573
+ _: 1
574
+ }, 8, ["content"])) : O("", !0) : (v(), b(t(Q), {
575
+ key: 0,
576
+ dot: "",
577
+ size: "sm"
578
+ }, {
579
+ default: p(() => [
580
+ z($(t(a)("outlet.not-published")), 1)
581
+ ]),
582
+ _: 1
583
+ })),
584
+ N("span", He, $(o.articles_count > 0 ? t(a)("outlet.articles-count", { count: o.articles_count }) : t(a)("outlet.no-articles")), 1),
585
+ o.locales.length > 0 ? (v(), D("span", {
586
+ key: 2,
587
+ class: "wx-outlet-row__locales",
588
+ "aria-label": t(a)("outlet.seen-in", { locales: o.locales.join(", ") })
589
+ }, $(o.locales.join(" · ")), 9, Ye)) : O("", !0)
590
+ ])
591
+ ])
592
+ ]),
593
+ _: 2
594
+ }, 1032, ["type", "class", "onClick"]))
595
+ ]),
596
+ actions: p(({ item: o }) => [
597
+ w(t(Ie), {
598
+ actions: G(o),
599
+ label: o.title
600
+ }, null, 8, ["actions", "label"])
601
+ ]),
602
+ _: 1
603
+ }, 8, ["modelValue", "class", "handle", "item-label", "disabled"]),
604
+ y.value && !m.value ? (v(), b(t(Be), {
605
+ key: 0,
606
+ class: "wx-press__note",
607
+ type: "info",
608
+ description: t(T).hint.value
609
+ }, null, 8, ["description"])) : O("", !0)
610
+ ], 64))
611
+ ]),
612
+ detail: p(({ inline: o, back: V }) => [
613
+ g.value !== null ? (v(), b(Ee, {
614
+ id: g.value === "new" ? null : g.value,
615
+ key: String(g.value),
616
+ inline: o,
617
+ onBack: V,
618
+ onSaved: P,
619
+ onCreated: J,
620
+ onRemoved: F
621
+ }, null, 8, ["id", "inline", "onBack"])) : O("", !0)
622
+ ]),
623
+ empty: p(() => [
624
+ w(t(le), {
625
+ title: t(a)("outlet.choose"),
626
+ description: y.value ? t(a)("outlet.choose-help") : void 0
627
+ }, null, 8, ["title", "description"])
628
+ ]),
629
+ _: 1
630
+ }, 8, ["open", "detail-label"])
631
+ ]),
632
+ _: 1
633
+ })
634
+ ]),
635
+ _: 1
636
+ }, 8, ["view", "title", "views"]));
637
+ }
638
+ }), Xe = /* @__PURE__ */ pe(Qe, [["__scopeId", "data-v-7ef16f83"]]);
639
+ function at(u = {}) {
640
+ const d = u.path ?? "/press";
641
+ return {
642
+ id: "press",
643
+ path: d,
644
+ // One route: the open outlet is in the address (`?outlet=`), beside the list it was opened
645
+ // from, so a link to it is a link to both.
646
+ routes: [{ path: d, name: "webx.press", component: Xe, props: { base: d } }]
647
+ };
648
+ }
649
+ export {
650
+ Z as PRESS_API,
651
+ Ee as WxOutletPane,
652
+ Xe as WxPressPage,
653
+ de as createPressApi,
654
+ at as press,
655
+ Re as pressMessages
656
+ };
657
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/api.ts","../src/messages.ts","../src/i18n.ts","../src/OutletPane.vue","../src/PressPage.vue","../src/module.ts"],"sourcesContent":["import { reorderItems, type AdminContext } from '@webx-ui/module-admin'\nimport type { ScreenModel } from '@webx-ui/schema'\nimport type { OutletDetail, OutletQuery, OutletRow, OutletsList } from './types'\n\nexport interface PressApi {\n list(query?: OutletQuery): Promise<OutletsList>\n get(id: number): Promise<OutletDetail>\n /**\n * A new outlet out of the values of its form, articles included, in one request: the form is\n * filled in first and the record made on the first save, so nobody leaves an empty outlet behind.\n */\n create(values: ScreenModel): Promise<OutletDetail>\n /** Refused with a 422 under the name of the field — `articles.<n>.<field>` for an article. */\n save(id: number, values: ScreenModel): Promise<OutletDetail>\n remove(id: number): Promise<void>\n restore(id: number): Promise<OutletRow>\n /** The order of the whole list. The order of the articles is the order of the form's rows. */\n reorder(ids: number[]): Promise<void>\n}\n\n/** The path of the section under the panel's API, and of its order. */\nexport const PRESS_API = 'press'\n\n/** Everything under `/press`, below the panel's API path (§4.10). */\nexport function createPressApi(admin: AdminContext): PressApi {\n const base = `${admin.apiPath}/${PRESS_API}`\n const data = <T>(body: { data: T }): T => body.data\n\n return {\n list: (query = {}) => {\n const search = new URLSearchParams()\n\n if (query.search) search.set('search', query.search)\n if (query.trashed) search.set('trashed', '1')\n\n return admin.http.get<OutletsList>(search.size > 0 ? `${base}?${search}` : base)\n },\n get: (id) => admin.http.get<{ data: OutletDetail }>(`${base}/${id}`).then(data),\n create: (values) => admin.http.post<{ data: OutletDetail }>(base, { values }).then(data),\n save: (id, values) =>\n admin.http.put<{ data: OutletDetail }>(`${base}/${id}`, { values }).then(data),\n remove: (id) => admin.http.delete<void>(`${base}/${id}`).then(() => undefined),\n // The resource of the list line, and not the form's answer: a restored outlet stays closed.\n restore: (id) => admin.http.post<{ data: OutletRow }>(`${base}/${id}/restore`, {}).then(data),\n reorder: (ids) => reorderItems(admin, PRESS_API, ids, null),\n }\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-press::` and overrides these;\n * `messages.test.ts` keeps the two sets of keys equal.\n *\n * `screen` is what the described screen `press.outlet-form` is labelled with: the server sends the\n * labels as `trans::webx-press::screen.*`, and these are what they say when no dictionary came.\n *\n * `kinds` are the four of the default config (decision 3). A kind a site adds brings its word in\n * `lang/vendor/webx-press` — the server labels the options of the field itself, so a kind missing\n * here is only missing from a panel that runs with no server at all.\n */\nexport const pressMessages: Record<string, Messages> = {\n module: {\n press: 'Press',\n },\n outlet: {\n new: 'New outlet',\n untitled: 'Untitled',\n search: 'Search the outlets',\n empty: 'No outlets yet.',\n 'empty-help':\n 'An outlet is a magazine, a paper or a site that wrote about you; its articles are added on its form.',\n 'empty-search': 'Nothing matches that.',\n 'empty-bin': 'The bin is empty.',\n all: 'All',\n trashed: 'Bin',\n 'not-published': 'Not published',\n 'visible-nowhere': 'Published, but seen in no language: none of its articles has a title yet.',\n 'articles-count': 'Articles: :count',\n 'no-articles': 'No articles yet',\n featured: 'In the logo strip',\n 'seen-in': 'Seen in: :locales',\n choose: 'Choose an outlet',\n 'choose-help': 'Or add a new one: the first line of the list.',\n save: 'Save',\n saved: 'The outlet is saved.',\n 'save-failed': 'The outlet was not saved.',\n cancel: 'Cancel',\n delete: 'Delete',\n 'delete-title': 'Delete “:name”?',\n 'delete-text':\n 'It goes to the bin with its articles and leaves the site and every block. Restored, it comes back with them.',\n deleted: 'The outlet is in the bin.',\n restore: 'Restore',\n restored: 'The outlet is back.',\n 'reorder-failed': 'The new order was not saved.',\n 'leave-title': 'Leave without saving?',\n 'leave-text': 'What you wrote in this outlet is not on the server.',\n leave: 'Leave',\n back: 'Back to the list',\n open: 'Open on the site',\n 'address-moving': 'The page of the outlet moves to the new address when you save.',\n },\n screen: {\n general: 'General',\n outlet: 'Outlet',\n logo: 'Logo',\n 'logo-help': 'Without a logo the site prints the name.',\n title: 'Name',\n 'title-help': 'Where it is not written in a language, the name from another one is shown.',\n slug: 'Address',\n 'slug-help':\n \"The outlet's page. A language with articles and no address of its own takes this one.\",\n 'website-url': 'Website',\n 'website-url-help': 'An address that starts with http:// or https://.',\n summary: 'About the outlet',\n 'summary-help':\n 'A few words under the name on its page, and its description for search engines.',\n settings: 'Settings',\n published: 'Published',\n 'published-help': 'On the site at once — in the languages it has articles in.',\n featured: 'In the strip of logos',\n 'featured-help': 'Shown by the logos block set to the marked outlets only.',\n articles: 'Articles',\n 'articles-help': \"In the order they are shown on the outlet's page. Saved with the outlet.\",\n 'article-label': '#:number · :title',\n 'article-untitled': 'Untitled',\n 'article-title': 'Title',\n 'article-title-help': 'An article is shown in a language only when it has a title in it.',\n excerpt: 'Excerpt',\n kind: 'Kind',\n 'kind-none': 'No kind',\n 'published-on': 'Date',\n 'date-precision': 'Known to',\n 'date-precision-help': 'How much of the date is printed: the day, the month, or the year only.',\n precision: {\n day: 'Day',\n month: 'Month',\n year: 'Year',\n },\n url: 'Link',\n 'url-help': \"The article on the outlet's site. Without it the title leads to the PDF.\",\n file: 'PDF',\n 'file-help': 'A PDF from the library — a scan of the page, say.',\n hidden: 'Do not show',\n 'hidden-help': 'Kept here, and shown nowhere on the site.',\n seo: 'SEO',\n 'seo-empty': 'The SEO card appears here when module-seo is installed.',\n },\n kinds: {\n mention: 'Mention',\n interview: 'Interview',\n expert_comment: 'Expert comment',\n authored: 'Authored article',\n },\n}\n","import { useAdmin } from '@webx-ui/module-admin'\nimport { pressMessages } 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 usePressMessages(): void {\n try {\n const { i18n } = useAdmin()\n\n if (!seeded.has(i18n)) {\n i18n.defaults('webx-press', pressMessages)\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, 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 provideRecordAddress,\n useAdmin,\n useErrorText,\n useTranslate,\n WxSaveState,\n WxScreen,\n WxScreenHead,\n type ScreenAction,\n} from '@webx-ui/module-admin'\nimport type { ScreenModel } from '@webx-ui/schema'\nimport { createPressApi } from './api'\nimport { usePressMessages } from './i18n'\nimport type { OutletDetail, OutletSummary } from './types'\n\n/**\n * One outlet with its articles: the pane beside the list, and the whole screen on a phone.\n *\n * The described screen `press.outlet-form` — General · Articles · SEO — with a head over it and\n * one button under it. One explicit save for the whole outlet, articles included (decision 15):\n * the articles are rows of its form, and the server writes them into their own table in the same\n * transaction (§4.6). No autosave: there is no draft (decision 9), so what is saved is on the site\n * at once — the button is the moment somebody decides it is ready.\n *\n * A new outlet is a form first and a record on the first save: an empty row made by pressing\n * \"New outlet\" and walking away would be an outlet 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: 'WxOutletPane' })\n\nconst props = withDefaults(\n defineProps<{\n /** `null` — an outlet not written yet. */\n id: number | null\n /** False in the drawer a narrow screen opens this in. */\n inline?: boolean\n }>(),\n { inline: true },\n)\n\nconst emit = defineEmits<{\n back: []\n /** The outlet was written; the list redraws its row. */\n saved: [outlet: OutletSummary]\n /** The first save of a new outlet: it is a record now, with an id to be opened by. */\n created: [outlet: OutletSummary]\n removed: [id: number]\n}>()\n\nconst context = useAdmin()\nconst api = createPressApi(context)\nconst locales = useLocales()\nusePressMessages()\n\nconst t = useTranslate('webx-press')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst outlet = ref<OutletSummary | null>(null)\nconst values = ref<ScreenModel>({})\nconst prefix = ref<string | null>(null)\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('press.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 name 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.title as LocalizedValue | string | null | undefined\n const fallback = props.id === null ? t('outlet.new') : (outlet.value?.title ?? '')\n\n return localizedValue(written, locales.active.value, fallback) || t('outlet.untitled')\n})\n\n/*\n * The address field is the panel's shared one (`wx-slug`), and this is what it prints. The form\n * answers with the page's address (`url`) in the panel's language — `/en/press/…` on a site whose\n * default is another — while the field compares it with the slug in the language being edited.\n * So the address is handed over without the host, the slash and the language, and only in its own\n * language: in any other the field has nothing to compare with, and says nothing rather than\n * warning that a page moves which does not.\n */\nprovideRecordAddress({\n values,\n prefix,\n path: computed(() => pathIn(outlet.value?.url ?? null, locales.active.value)),\n moving: () => t('outlet.address-moving'),\n})\n\nfunction pathIn(url: string | null, locale: string): string | null | undefined {\n if (url === null) return null\n\n let segments: string[]\n\n try {\n segments = new URL(url, 'http://site.invalid').pathname.split('/').filter((one) => one !== '')\n } catch {\n return null\n }\n\n const codes = locales.list.value.map((one) => one.code)\n const own =\n codes.includes(segments[0] ?? '') && segments[0] !== codes[0] ? segments.shift()! : codes[0]\n\n return own === undefined || own === locale ? segments.join('/') : undefined\n}\n\nfunction take(detail: OutletDetail): void {\n outlet.value = detail.outlet\n values.value = detail.values\n prefix.value = detail.prefix\n snapshot.value = JSON.stringify(detail.values)\n}\n\nif (props.id === null) {\n // What the server would answer for an outlet with nothing in it yet.\n const blank: ScreenModel = {\n title: {},\n slug: {},\n summary: {},\n published: false,\n featured: false,\n articles: [],\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('outlet.saved'))\n if (made) emit('created', detail.outlet)\n else emit('saved', detail.outlet)\n } catch (error) {\n const body = (error as { body?: { errors?: Record<string, string[]> } }).body\n\n if (body?.errors) {\n // `articles.2.url` as the server names it: the repeater finds its row and field by that.\n errors.value = body.errors\n toast.danger(t('outlet.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 record = outlet.value\n\n if (!record) return\n\n const agreed = await confirm({\n title: t('outlet.delete-title', { name: record.title }),\n message: t('outlet.delete-text'),\n confirmText: t('outlet.delete'),\n cancelText: t('outlet.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.remove(record.id)\n toast.success(t('outlet.deleted'))\n // Nothing left to protect: the record is in the bin.\n snapshot.value = current.value\n emit('removed', record.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\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('outlet.leave-title'),\n message: t('outlet.leave-text'),\n confirmText: t('outlet.leave'),\n cancelText: t('outlet.cancel'),\n tone: 'danger',\n })\n}\n\n/*\n * The list and this form are one route, so another outlet is an update of it rather than a way\n * out — and so is every keystroke in the search above the list, which goes into the address too.\n * Only a change of the outlet asks.\n */\nonBeforeRouteUpdate(async (to: RouteLocationNormalized, from: RouteLocationNormalized) =>\n to.query.outlet === from.query.outlet ? true : await mayLeave(),\n)\n\nonBeforeRouteLeave(mayLeave)\n\n/* What leads away from the outlet. On a narrow pane the head folds them into the ···. */\nconst actions = computed<ScreenAction[]>(() => {\n const record = outlet.value\n const list: ScreenAction[] = []\n\n // Only a page that answers: an unpublished outlet, or one seen nowhere, has no address.\n if (record?.url) {\n const url = record.url\n\n list.push({\n key: 'open',\n label: t('outlet.open'),\n icon: 'external-link',\n run: () => void window.open(url, '_blank', 'noopener'),\n })\n }\n\n if (canManage.value && record !== null) {\n list.push({\n key: 'delete',\n label: t('outlet.delete'),\n icon: 'trash',\n danger: true,\n menu: true,\n run: () => void remove(),\n })\n }\n\n return list\n})\n</script>\n\n<template>\n <div class=\"wx-outlet\" @keydown=\"onKeydown\">\n <wx-skeleton v-if=\"loading\" class=\"wx-outlet__ghost\" title :rows=\"6\" />\n\n <template v-else>\n <wx-screen-head\n :level=\"3\"\n :back=\"!props.inline\"\n :back-label=\"t('outlet.back')\"\n :title=\"title\"\n :actions=\"actions\"\n :collapse-below=\"480\"\n @back=\"emit('back')\"\n >\n <template v-if=\"outlet && !outlet.published\" #title-after>\n <wx-badge dot>{{ t('outlet.not-published') }}</wx-badge>\n </template>\n </wx-screen-head>\n\n <wx-screen\n v-model=\"values\"\n name=\"press.outlet-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('outlet.save') }}\n </wx-button>\n </wx-action-bar>\n </template>\n </div>\n</template>\n\n<style scoped>\n.wx-outlet {\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-outlet__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 WxAvatar,\n WxBadge,\n WxCard,\n WxEmpty,\n WxIcon,\n WxInput,\n WxListDetail,\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 { createPressApi, PRESS_API } from './api'\nimport { usePressMessages } from './i18n'\nimport OutletPane from './OutletPane.vue'\nimport type { OutletRow, OutletSummary } from './types'\n\n/**\n * The outlets: all of them on the left, the one being written on the right (§4.9, decision 15).\n *\n * The shape of the reviews, for the same reason: an outlet is typed in from a clipping in a\n * minute, and the next one is usually under it. On a phone the form is the whole screen, in the\n * drawer `WxListDetail` raises, and draws its own way back.\n *\n * One order, the general one (decision 6): a drag moves it; a search or the bin shows a\n * selection, so the grips go away and the line under the list says why. The order of the\n * articles is not here — it is the order of the rows in the outlet's form.\n *\n * Everything the screen is looking at lives in the address — the view, the search and the open\n * outlet — so a link to an outlet is a link to it in its list.\n */\nwithDefaults(defineProps<{ base?: string }>(), { base: '/press' })\n\nconst context = useAdmin()\nconst api = createPressApi(context)\nconst route = useRoute()\nconst router = useRouter()\nusePressMessages()\n\nconst t = useTranslate('webx-press')\n/* Not the server's `message`: the panel says how a request failed in its own words. */\nconst message = useErrorText()\n\nconst rows = ref<OutletRow[]>([])\nconst loading = ref(true)\n\nconst canManage = computed(() => context.can('press.manage'))\n\nconst title = computed(\n () =>\n context.state.manifest?.modules.find((module) => module.id === 'press')?.title ??\n t('module.press'),\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 // An outlet from the list is not an outlet from the bin: the form closes with the view.\n delete query.outlet\n void router.replace({ query })\n },\n})\n\nconst inBin = computed(() => view.value === 'trashed')\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 outlet: a number, `'new'` for one not yet saved, or nothing. */\nconst selected = computed<number | 'new' | null>(() => {\n const raw = route.query.outlet\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 \"an outlet 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('outlet.all') },\n { value: 'trashed', label: t('outlet.trashed'), icon: 'trash' },\n])\n\nconst order = useItemOrder(PRESS_API, () => ({ q: q.value, filtered: inBin.value }))\n\n/* Dragged only by somebody who may write, and only in a list that is the whole order. */\nconst sortable = computed(() => canManage.value && order.sortable.value)\n\nconst emptyText = computed(() => {\n if (q.value !== '') return t('outlet.empty-search')\n\n return inBin.value ? t('outlet.empty-bin') : t('outlet.empty')\n})\n\nasync function load(): Promise<void> {\n loading.value = true\n\n try {\n const answer = await api.list({ search: q.value, trashed: inBin.value })\n\n rows.value = answer.data\n } catch (error) {\n toast.danger(message(error))\n } finally {\n loading.value = false\n }\n}\n\nwatch(\n () => [view.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\n/* `replace` and not `push`: going through twenty outlets is not twenty steps of history. */\nfunction choose(id: number | 'new'): void {\n void router.replace({ query: { ...route.query, outlet: String(id) } })\n}\n\nfunction close(): void {\n const query = { ...route.query }\n\n delete query.outlet\n void router.replace({ query })\n}\n\n/** A new outlet was saved: it is a record now, and the list has it. */\nfunction created(outlet: OutletSummary): void {\n choose(outlet.id)\n void load()\n}\n\n/**\n * The order on screen, written as a whole. The list is already in its new order\n * (`WxSortableList` reorders the model before it says so), so a failure has to put it back\n * 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('outlet.reorder-failed')))\n await load()\n }\n}\n\nfunction actionsFor(row: OutletRow): RowAction[] {\n if (!canManage.value) return []\n\n if (inBin.value) {\n return [\n {\n key: 'restore',\n icon: 'refresh',\n label: t('outlet.restore'),\n run: () => void restore(row),\n },\n ]\n }\n\n return [\n {\n key: 'delete',\n icon: 'trash',\n label: t('outlet.delete'),\n danger: true,\n run: () => void remove(row),\n },\n ]\n}\n\nasync function remove(row: OutletRow): Promise<void> {\n const agreed = await confirm({\n title: t('outlet.delete-title', { name: row.title }),\n message: t('outlet.delete-text'),\n confirmText: t('outlet.delete'),\n cancelText: t('outlet.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.remove(row.id)\n toast.success(t('outlet.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: OutletRow): Promise<void> {\n try {\n await api.restore(row.id)\n toast.success(t('outlet.restored'))\n } catch (error) {\n toast.danger(message(error))\n } finally {\n await load()\n }\n}\n\n/* Published and yet shown in no language: the one state the list has to point out. */\nfunction unseen(row: OutletRow): 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-press\" padding=\"none\">\n <wx-list-detail\n v-model:open=\"open\"\n class=\"wx-press__panes\"\n :list-width=\"360\"\n :detail-min=\"520\"\n :detail-label=\"t('module.press')\"\n >\n <template #list>\n <div class=\"wx-press__bar\">\n <wx-input\n v-model=\"typed\"\n class=\"wx-press__search\"\n size=\"sm\"\n clearable\n :placeholder=\"t('outlet.search')\"\n :aria-label=\"t('outlet.search')\"\n >\n <template #prefix><wx-icon name=\"search\" size=\"sm\" /></template>\n </wx-input>\n </div>\n\n <!--\n A line of the list rather than a button in the head: the new outlet is written where\n it will stand, beside the ones it joins — and the form opens the way any other does.\n -->\n <button\n v-if=\"canManage && !inBin\"\n type=\"button\"\n class=\"wx-press__new\"\n :class=\"{ 'is-current': selected === 'new' }\"\n @click=\"choose('new')\"\n >\n <wx-icon name=\"plus\" size=\"sm\" />\n {{ t('outlet.new') }}\n </button>\n\n <wx-skeleton v-if=\"loading && rows.length === 0\" class=\"wx-press__loading\" :rows=\"5\" />\n\n <wx-empty\n v-else-if=\"rows.length === 0\"\n :title=\"emptyText\"\n :description=\"!inBin && q === '' ? t('outlet.empty-help') : undefined\"\n />\n\n <template v-else>\n <!-- A grip and not the whole row: the row opens an outlet, and a row that both opens\n and drags is one where one of the two happens by accident. -->\n <wx-sortable-list\n v-model=\"rows\"\n class=\"wx-press__list\"\n :class=\"{ 'is-loading': loading }\"\n plain\n item-key=\"id\"\n :handle=\"sortable ? 'grip' : 'row'\"\n :item-label=\"(item: OutletRow) => item.title\"\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-outlet-row\"\n :class=\"{ 'is-current': selected === item.id }\"\n @click=\"inBin ? undefined : choose(item.id)\"\n >\n <!-- A logo is wide as often as not: in a square it shrinks to a sliver, so it gets a\n box of its own shape, and only an outlet without one gets initials. -->\n <span v-if=\"item.logo?.thumb\" class=\"wx-outlet-row__logo\">\n <img :src=\"item.logo.thumb\" alt=\"\" />\n </span>\n <wx-avatar\n v-else\n class=\"wx-outlet-row__initials\"\n shape=\"square\"\n :name=\"item.title\"\n size=\"md\"\n />\n\n <span class=\"wx-outlet-row__body\">\n <span class=\"wx-outlet-row__head\">\n <span class=\"wx-outlet-row__title\">{{ item.title }}</span>\n <wx-tooltip v-if=\"item.featured\" :content=\"t('outlet.featured')\">\n <wx-icon\n class=\"wx-outlet-row__featured\"\n name=\"star\"\n size=\"sm\"\n :aria-label=\"t('outlet.featured')\"\n />\n </wx-tooltip>\n </span>\n\n <span class=\"wx-outlet-row__facts\">\n <wx-badge v-if=\"!item.published\" dot size=\"sm\">\n {{ t('outlet.not-published') }}\n </wx-badge>\n <wx-tooltip v-else-if=\"unseen(item)\" :content=\"t('outlet.visible-nowhere')\">\n <wx-badge type=\"warning\" dot size=\"sm\">\n {{ t('outlet.not-published') }}\n </wx-badge>\n </wx-tooltip>\n\n <span class=\"wx-outlet-row__count\">\n {{\n item.articles_count > 0\n ? t('outlet.articles-count', { count: item.articles_count })\n : t('outlet.no-articles')\n }}\n </span>\n\n <span\n v-if=\"item.locales.length > 0\"\n class=\"wx-outlet-row__locales\"\n :aria-label=\"t('outlet.seen-in', { locales: item.locales.join(', ') })\"\n >\n {{ item.locales.join(' · ') }}\n </span>\n </span>\n </span>\n </component>\n </template>\n\n <template #actions=\"{ item }\">\n <wx-row-menu :actions=\"actionsFor(item)\" :label=\"item.title\" />\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-press__note\"\n type=\"info\"\n :description=\"order.hint.value\"\n />\n </template>\n </template>\n\n <template #detail=\"{ inline, back }\">\n <outlet-pane\n v-if=\"selected !== null\"\n :id=\"selected === 'new' ? null : selected\"\n :key=\"String(selected)\"\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('outlet.choose')\"\n :description=\"canManage ? t('outlet.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-press > .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-press__bar {\n display: flex;\n gap: var(--wx-space-8);\n padding: var(--wx-space-12) var(--wx-space-12) var(--wx-space-8);\n}\n\n.wx-press__search {\n flex: 1 1 180px;\n min-width: 0;\n}\n\n.wx-press__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-press__new:hover,\n.wx-press__new.is-current {\n background: var(--wx-bg-subtle);\n}\n\n.wx-press__loading {\n padding: var(--wx-space-12);\n}\n\n.wx-press__list {\n padding-inline: var(--wx-space-8);\n}\n\n.wx-press__list.is-loading {\n opacity: 0.6;\n}\n\n.wx-press__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-press__list :deep(.wx-sortable-list__row:hover),\n.wx-press__list :deep(.wx-sortable-list__row:has(.is-current)) {\n background: var(--wx-bg-subtle);\n}\n\n.wx-press__note {\n margin: var(--wx-space-8);\n}\n\n.wx-outlet-row {\n display: flex;\n align-items: flex-start;\n gap: var(--wx-space-10);\n /* The row is the target: a short name should not leave half the line dead — and a button,\n 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-outlet-row {\n cursor: pointer;\n}\n\n.wx-outlet-row__initials {\n flex: none;\n}\n\n/* Twice as wide as the initials beside it, and the logo drawn whole inside: cropped, it loses\n its letters. */\n.wx-outlet-row__logo {\n display: flex;\n flex: none;\n align-items: center;\n justify-content: center;\n width: 64px;\n height: 32px;\n overflow: hidden;\n border-radius: var(--wx-radius-sm);\n background: var(--wx-bg-subtle);\n}\n\n.wx-outlet-row__logo img {\n display: block;\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n}\n\n.wx-outlet-row__body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--wx-space-2);\n min-width: 0;\n}\n\n.wx-outlet-row__head {\n display: flex;\n align-items: center;\n gap: var(--wx-space-6);\n min-width: 0;\n}\n\n.wx-outlet-row__title {\n min-width: 0;\n overflow: hidden;\n font-weight: var(--wx-font-weight-medium);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.wx-outlet-row.is-current .wx-outlet-row__title {\n color: var(--wx-color-primary);\n}\n\n.wx-outlet-row__featured {\n flex: none;\n color: var(--wx-color-warning);\n}\n\n.wx-outlet-row__facts {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--wx-space-4) var(--wx-space-8);\n min-width: 0;\n color: var(--wx-text-muted);\n font-size: var(--wx-font-size-sm);\n}\n\n.wx-outlet-row__locales {\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 type { AdminModule } from '@webx-ui/module-admin'\nimport PressPage from './PressPage.vue'\n\nexport interface PressOptions {\n /** Where the outlets live inside the panel. */\n path?: string\n}\n\n/**\n * The press as a section of the panel (§4.9): one module and one entry, without a group of its\n * own — the reviews have one because they have two entries, and a group around a single item is\n * a heading over itself. A section whose server half is not installed never appears — the entry\n * is built from the manifest.\n */\nexport function press(options: PressOptions = {}): AdminModule {\n const path = options.path ?? '/press'\n\n return {\n id: 'press',\n path,\n // One route: the open outlet is in the address (`?outlet=`), beside the list it was opened\n // from, so a link to it is a link to both.\n routes: [{ path, name: 'webx.press', component: PressPage, props: { base: path } }],\n }\n}\n"],"names":["PRESS_API","createPressApi","admin","base","data","body","query","search","id","values","ids","reorderItems","pressMessages","seeded","usePressMessages","i18n","useAdmin","props","__props","emit","__emit","context","api","locales","useLocales","t","useTranslate","message","useErrorText","outlet","ref","prefix","snapshot","loading","saving","errors","canManage","computed","current","dirty","state","title","written","fallback","localizedValue","provideRecordAddress","pathIn","url","locale","segments","one","codes","own","take","detail","blank","load","error","toast","save","made","remove","record","confirm","leaveGuard","event","onKeydown","onMounted","onBeforeUnmount","mayLeave","onBeforeRouteUpdate","to","from","onBeforeRouteLeave","actions","list","_createElementBlock","_createBlock","_unref","WxSkeleton","_Fragment","_createVNode","WxScreenHead","WxBadge","WxScreen","$event","WxActionBar","WxSaveState","WxButton","route","useRoute","router","useRouter","rows","module","view","value","inBin","q","typed","selected","raw","open","close","views","order","useItemOrder","sortable","emptyText","answer","watch","pause","choose","created","reorder","row","actionsFor","restore","removed","unseen","WxListScreen","WxCard","WxListDetail","_createElementVNode","_hoisted_1","WxInput","WxIcon","_normalizeClass","_createTextVNode","WxEmpty","WxSortableList","item","_withCtx","_openBlock","_resolveDynamicComponent","_hoisted_2","WxAvatar","_hoisted_4","_hoisted_5","_hoisted_6","_toDisplayString","WxTooltip","_hoisted_7","_hoisted_8","_hoisted_9","WxRowMenu","WxAlert","inline","back","OutletPane","press","options","path","PressPage"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { Messages } from '@webx-ui/module-admin';
2
+ /**
3
+ * The English this package speaks on its own, before the server's dictionary arrives, or without
4
+ * one. The server ships the same lines in ten languages under `webx-press::` and overrides these;
5
+ * `messages.test.ts` keeps the two sets of keys equal.
6
+ *
7
+ * `screen` is what the described screen `press.outlet-form` is labelled with: the server sends the
8
+ * labels as `trans::webx-press::screen.*`, and these are what they say when no dictionary came.
9
+ *
10
+ * `kinds` are the four of the default config (decision 3). A kind a site adds brings its word in
11
+ * `lang/vendor/webx-press` — the server labels the options of the field itself, so a kind missing
12
+ * here is only missing from a panel that runs with no server at all.
13
+ */
14
+ export declare const pressMessages: Record<string, Messages>;
@@ -0,0 +1,12 @@
1
+ import { AdminModule } from '@webx-ui/module-admin';
2
+ export interface PressOptions {
3
+ /** Where the outlets live inside the panel. */
4
+ path?: string;
5
+ }
6
+ /**
7
+ * The press as a section of the panel (§4.9): one module and one entry, without a group of its
8
+ * own — the reviews have one because they have two entries, and a group around a single item is
9
+ * a heading over itself. A section whose server half is not installed never appears — the entry
10
+ * is built from the manifest.
11
+ */
12
+ export declare function press(options?: PressOptions): AdminModule;
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .wx-outlet[data-v-396f08f3]{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-outlet__ghost[data-v-396f08f3]{max-width:480px}.wx-press>.wx-card__body{display:flex;flex-direction:column;overflow:clip;border-radius:inherit}.wx-press__bar[data-v-7ef16f83]{display:flex;gap:var(--wx-space-8);padding:var(--wx-space-12) var(--wx-space-12) var(--wx-space-8)}.wx-press__search[data-v-7ef16f83]{flex:1 1 180px;min-width:0}.wx-press__new[data-v-7ef16f83]{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-press__new[data-v-7ef16f83]:hover,.wx-press__new.is-current[data-v-7ef16f83]{background:var(--wx-bg-subtle)}.wx-press__loading[data-v-7ef16f83]{padding:var(--wx-space-12)}.wx-press__list[data-v-7ef16f83]{padding-inline:var(--wx-space-8)}.wx-press__list.is-loading[data-v-7ef16f83]{opacity:.6}.wx-press__list[data-v-7ef16f83] .wx-sortable-list__row{gap:var(--wx-space-4);padding-inline:var(--wx-space-4);border-radius:var(--wx-radius-sm)}.wx-press__list[data-v-7ef16f83] .wx-sortable-list__row:hover,.wx-press__list[data-v-7ef16f83] .wx-sortable-list__row:has(.is-current){background:var(--wx-bg-subtle)}.wx-press__note[data-v-7ef16f83]{margin:var(--wx-space-8)}.wx-outlet-row[data-v-7ef16f83]{display:flex;align-items:flex-start;gap:var(--wx-space-10);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-outlet-row[data-v-7ef16f83]{cursor:pointer}.wx-outlet-row__initials[data-v-7ef16f83]{flex:none}.wx-outlet-row__logo[data-v-7ef16f83]{display:flex;flex:none;align-items:center;justify-content:center;width:64px;height:32px;overflow:hidden;border-radius:var(--wx-radius-sm);background:var(--wx-bg-subtle)}.wx-outlet-row__logo img[data-v-7ef16f83]{display:block;max-width:100%;max-height:100%;object-fit:contain}.wx-outlet-row__body[data-v-7ef16f83]{display:flex;flex:1 1 auto;flex-direction:column;gap:var(--wx-space-2);min-width:0}.wx-outlet-row__head[data-v-7ef16f83]{display:flex;align-items:center;gap:var(--wx-space-6);min-width:0}.wx-outlet-row__title[data-v-7ef16f83]{min-width:0;overflow:hidden;font-weight:var(--wx-font-weight-medium);text-overflow:ellipsis;white-space:nowrap}.wx-outlet-row.is-current .wx-outlet-row__title[data-v-7ef16f83]{color:var(--wx-color-primary)}.wx-outlet-row__featured[data-v-7ef16f83]{flex:none;color:var(--wx-color-warning)}.wx-outlet-row__facts[data-v-7ef16f83]{display:flex;flex-wrap:wrap;align-items:center;gap:var(--wx-space-4) var(--wx-space-8);min-width:0;color:var(--wx-text-muted);font-size:var(--wx-font-size-sm)}.wx-outlet-row__locales[data-v-7ef16f83]{font-family:var(--wx-font-family-mono);font-size:var(--wx-font-size-xs);text-transform:uppercase}
@@ -0,0 +1,71 @@
1
+ import { ScreenModel } from '@webx-ui/schema';
2
+ /**
3
+ * One outlet as the section lists it (§4.10).
4
+ *
5
+ * `locales` is where it can be seen — the languages at least one of its articles has a title in —
6
+ * and it is a separate thing from `published` on purpose: "published, and seen nowhere" is the
7
+ * row the list has to point out (decision 7).
8
+ */
9
+ export interface OutletRow {
10
+ id: number;
11
+ /** In the language the panel is open in, in any other where there is none, `#id` where none. */
12
+ title: string;
13
+ logo: {
14
+ thumb: string | null;
15
+ } | null;
16
+ published: boolean;
17
+ /** In the logo strip (decision 13). */
18
+ featured: boolean;
19
+ /** Its place in the whole list. */
20
+ position: number;
21
+ locales: string[];
22
+ articles_count: number;
23
+ updated_at: string | null;
24
+ deleted_at: string | null;
25
+ }
26
+ /** Every outlet — no pages: the list is where outlets are put in order (decision 6). */
27
+ export interface OutletsList {
28
+ data: OutletRow[];
29
+ }
30
+ export interface OutletQuery {
31
+ /** The name, in any language the site has. */
32
+ search?: string;
33
+ /** What was deleted. The only way back to an outlet in the bin is through this list. */
34
+ trashed?: boolean;
35
+ }
36
+ /** The record half of the form's answer — what the head of the form needs. */
37
+ export interface OutletSummary {
38
+ id: number;
39
+ title: string;
40
+ published: boolean;
41
+ deleted_at: string | null;
42
+ /** Its page on the site in the panel's language; `null` when it has none there or at all. */
43
+ url: string | null;
44
+ }
45
+ /**
46
+ * One outlet as its form opens it: the record, the values of `press.outlet-form` by field name —
47
+ * its articles among them, as rows in their order — and where its addresses start (`null` when
48
+ * the site has no outlet pages, `pages = false`).
49
+ */
50
+ export interface OutletDetail {
51
+ outlet: OutletSummary;
52
+ values: ScreenModel;
53
+ prefix: string | null;
54
+ }
55
+ /** One row of `articles` in the form's values (§4.10). */
56
+ export interface ArticleValues {
57
+ /** Absent on a row not saved yet: the server makes it then. */
58
+ id?: number;
59
+ title: Record<string, string>;
60
+ excerpt: Record<string, string>;
61
+ kind: string | null;
62
+ /** `Y-m-d`. */
63
+ published_on: string | null;
64
+ date_precision: 'day' | 'month' | 'year';
65
+ url: string | null;
66
+ file: {
67
+ path: string;
68
+ } | null;
69
+ is_hidden: boolean;
70
+ [field: string]: unknown;
71
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@webx-ui/module-press",
3
+ "version": "0.1.0",
4
+ "description": "Press for the WebX UI admin panel: the outlets that wrote about a site and their articles — a link or a PDF — ordered by hand, shown on the site by blocks.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": [
8
+ "*.css"
9
+ ],
10
+ "keywords": [
11
+ "webx-ui",
12
+ "vue",
13
+ "admin",
14
+ "cms",
15
+ "press",
16
+ "media-coverage"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/webx-ui/webx-ui.git",
21
+ "directory": "packages/module-press"
22
+ },
23
+ "homepage": "https://webx-ui.github.io/webx-ui/guide/press.html",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "main": "./dist/index.js",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js"
37
+ },
38
+ "./style.css": "./dist/style.css",
39
+ "./dist/style.css": "./dist/style.css",
40
+ "./package.json": "./package.json"
41
+ },
42
+ "peerDependencies": {
43
+ "vue": "^3.5.0",
44
+ "vue-router": "^4.5.0"
45
+ },
46
+ "dependencies": {
47
+ "@webx-ui/core": "^0.34.0",
48
+ "@webx-ui/module-admin": "^0.18.1",
49
+ "@webx-ui/schema": "^0.6.2"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^24.10.1",
53
+ "@vitejs/plugin-vue": "^6.0.1",
54
+ "typescript": "^5.9.3",
55
+ "vite": "^7.1.14",
56
+ "vite-plugin-dts": "^4.5.4",
57
+ "vue": "^3.5.24",
58
+ "vue-router": "^4.5.1",
59
+ "vue-tsc": "^3.1.3"
60
+ },
61
+ "scripts": {
62
+ "build": "vite build",
63
+ "dev": "vite build --watch",
64
+ "typecheck": "vue-tsc -p tsconfig.json --noEmit"
65
+ }
66
+ }