@webx-ui/module-pages 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,63 @@
1
+ # @webx-ui/module-pages
2
+
3
+ The front end of the pages section of the [WebX UI](https://github.com/webx-ui/webx-ui) admin
4
+ panel: the site's pages as a tree, their addresses, and the dialogs that create and move them.
5
+
6
+ The other half is the Composer package `webx-ui/module-pages`, which owns the tree, the
7
+ addresses and the API. A section appears in the panel when both halves are installed.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @webx-ui/module-pages
13
+ ```
14
+
15
+ ```ts
16
+ import { createAdmin } from '@webx-ui/module-admin'
17
+ import { pages } from '@webx-ui/module-pages'
18
+ import '@webx-ui/module-pages/style.css'
19
+
20
+ createAdmin({
21
+ basePath: '/cms',
22
+ modules: [pages()],
23
+ }).mount()
24
+ ```
25
+
26
+ `pages({ path: '/content' })` puts the section somewhere else inside the panel.
27
+
28
+ ## The list
29
+
30
+ One level of the tree at a time. The home page is pinned at the top and its children are the
31
+ level below it: every page of the site is inside the home page, so drawing it as a branch would
32
+ give every row a step of indentation that says nothing.
33
+
34
+ - **Children arrive when a branch is opened** — a catalogue of a few hundred pages is never
35
+ fetched whole.
36
+ - **Searching puts the tree away** and answers with a flat list of matches, each with its
37
+ address: a branch drawn for the sake of one match deep inside it tells the reader nothing.
38
+ - **The bin is a filter, not a section.** It lists the pages somebody deleted; whatever went
39
+ down with a page comes back with it.
40
+ - **Two ways to move a page:** drag it, or use “Move…” and pick the page it goes inside —
41
+ which is the one that works on a touch screen and in a big catalogue.
42
+
43
+ Moving a page rewrites every address under it and leaves a redirect on each of the old ones,
44
+ and the section says so out loud: an editor should not learn about a thousand redirects from a
45
+ search engine.
46
+
47
+ ## The API
48
+
49
+ `createPagesApi(context)` is the same set of calls the screen makes, for a panel that wants to
50
+ do something else with them:
51
+
52
+ ```ts
53
+ const api = createPagesApi(useAdmin())
54
+
55
+ await api.list({ parent: 12 })
56
+ await api.create({ title: 'About us', parent_id: 1 })
57
+ await api.move(12, 1, 'inside')
58
+ await api.remove(12) // to the bin, with the branch under it
59
+ ```
60
+
61
+ ## Licence
62
+
63
+ MIT
@@ -0,0 +1,36 @@
1
+ import { PageRow } from './types';
2
+ /**
3
+ * What a row offers, in a table cell or on a card.
4
+ *
5
+ * Written once as a list and drawn twice: a row is read by shape, a menu is read by name, and
6
+ * seven unlabelled icons in a dropdown would be worse than the row they replaced. On a card
7
+ * there is no row at all — at that width the card is the screen, and a menu of words is easier
8
+ * to hit than seven targets the size of a fingernail.
9
+ */
10
+ type __VLS_Props = {
11
+ page: PageRow;
12
+ inBin?: boolean;
13
+ /** Only the menu, for a card. */
14
+ menuOnly?: boolean;
15
+ };
16
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
17
+ open: (page: PageRow) => any;
18
+ duplicate: (page: PageRow) => any;
19
+ move: (page: PageRow) => any;
20
+ restore: (page: PageRow) => any;
21
+ remove: (page: PageRow) => any;
22
+ add: (page: PageRow) => any;
23
+ copy: (page: PageRow) => any;
24
+ }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
25
+ onOpen?: ((page: PageRow) => any) | undefined;
26
+ onDuplicate?: ((page: PageRow) => any) | undefined;
27
+ onMove?: ((page: PageRow) => any) | undefined;
28
+ onRestore?: ((page: PageRow) => any) | undefined;
29
+ onRemove?: ((page: PageRow) => any) | undefined;
30
+ onAdd?: ((page: PageRow) => any) | undefined;
31
+ onCopy?: ((page: PageRow) => any) | undefined;
32
+ }>, {
33
+ inBin: boolean;
34
+ menuOnly: boolean;
35
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
36
+ export default _default;
@@ -0,0 +1,16 @@
1
+ import { PageRow } from './types';
2
+ /**
3
+ * A new page: what it is called, where it lives, and what it is called in the address.
4
+ *
5
+ * The address follows the title while nobody has touched it, and stops the moment somebody
6
+ * does — a field that keeps rewriting what was typed into it is a field people fight.
7
+ */
8
+ type __VLS_Props = {
9
+ parent?: PageRow | null;
10
+ };
11
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
12
+ parent: PageRow | null;
13
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
14
+ field: HTMLDivElement;
15
+ }, any>;
16
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
2
+ export default _default;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The editor of one page: a head that stays put, and the described screen under it.
3
+ *
4
+ * What the page itself owns is the head — where this page sits, what state it is in, and the
5
+ * three things that change that state — and the saving. Everything below the head is
6
+ * `pages.form`, so a module or a project adds a tab to the editor with a patch (§12) rather
7
+ * than with a fork of this file.
8
+ *
9
+ * Saving is by autosave: a pause after the last keystroke, and the moment a field is left. The
10
+ * explicit button is still there, because a button that says "saved" is the only way a person
11
+ * can be sure — but nothing is ever lost by not pressing it.
12
+ */
13
+ type __VLS_Props = {
14
+ base?: string;
15
+ };
16
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
17
+ base: string;
18
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
19
+ root: HTMLDivElement;
20
+ }, any>;
21
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, HTMLDivElement>;
2
+ export default _default;
@@ -0,0 +1,15 @@
1
+ import { PageRow } from './types';
2
+ /**
3
+ * Where a page goes, chosen from a tree rather than dragged.
4
+ *
5
+ * Dragging is the quick way and it is in the table already; this is the one that works — on a
6
+ * touch screen, and in a catalogue where the page and its new parent are four screens apart.
7
+ *
8
+ * Inside, and only inside. Order among siblings is what dragging is for, and a dialog that also
9
+ * asked "before or after what" would be asking about the one thing nobody opened it for.
10
+ */
11
+ type __VLS_Props = {
12
+ page: PageRow;
13
+ };
14
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
15
+ export default _default;
@@ -0,0 +1,28 @@
1
+ import { PageRow } from './types';
2
+ /**
3
+ * A page chosen out of the tree — where a new page goes, or where a moved one lands.
4
+ *
5
+ * Lazy, like the section's own list: a catalogue is fetched a level at a time, and a picker
6
+ * that pulled the whole tree down to offer one parent would be the one screen of the section
7
+ * that does not.
8
+ */
9
+ type __VLS_Props = {
10
+ /** Pages that may not be chosen — the page being moved, above all. */
11
+ exclude?: number[];
12
+ placeholder?: string;
13
+ /** The page the value names, with its ancestors, so a lazy tree can show it unopened. */
14
+ selected?: PageRow[];
15
+ };
16
+ type __VLS_PublicProps = {
17
+ modelValue?: number | null;
18
+ } & __VLS_Props;
19
+ declare const _default: import('vue').DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
20
+ "update:modelValue": (value: number | null) => any;
21
+ }, string, import('vue').PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
22
+ "onUpdate:modelValue"?: ((value: number | null) => any) | undefined;
23
+ }>, {
24
+ placeholder: string;
25
+ exclude: number[];
26
+ selected: PageRow[];
27
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
28
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
2
+ export default _default;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The section: the site's pages as a tree, a level at a time.
3
+ *
4
+ * The home page is pinned at the top and its children are the top level, because every page of
5
+ * the site is inside it — drawn as a branch, every row of the table would carry one step of
6
+ * indentation that says nothing (§9).
7
+ *
8
+ * Searching puts the tree away. A branch drawn for the sake of one match deep inside it tells
9
+ * the reader nothing they asked about, so a search is a flat list of matches with the address
10
+ * under each.
11
+ */
12
+ type __VLS_Props = {
13
+ base?: string;
14
+ };
15
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
16
+ base: string;
17
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
18
+ root: HTMLDivElement;
19
+ }, HTMLDivElement>;
20
+ export default _default;
package/dist/api.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { AdminContext } from '@webx-ui/module-admin';
2
+ import { PageDetail, PageDropZone, PageInput, PageLevel, PageMoveResult, PageRow, PageSave, PageVersion } from './types';
3
+ export interface PagesApi {
4
+ /** One level of the tree, or a flat list when searching or looking in the bin. */
5
+ list(query?: {
6
+ parent?: number | null;
7
+ search?: string;
8
+ status?: string;
9
+ trashed?: boolean;
10
+ /** The whole tree in one flat list, in the order it reads top to bottom. */
11
+ flat?: boolean;
12
+ }): Promise<PageLevel>;
13
+ get(id: number): Promise<PageDetail>;
14
+ create(input: PageInput): Promise<PageRow>;
15
+ /**
16
+ * Save the draft. Refused with a 409 when the revision is not the current one — the error's
17
+ * body is a {@link PageConflict}, and the page it carries is the page as it now is.
18
+ */
19
+ save(id: number, input: PageSave): Promise<PageDetail>;
20
+ move(id: number, target: number, zone: PageDropZone): Promise<PageMoveResult>;
21
+ duplicate(id: number): Promise<PageRow>;
22
+ publish(id: number): Promise<PageRow>;
23
+ unpublish(id: number): Promise<PageRow>;
24
+ /** Into the bin, with the branch under it. Answers how many went. */
25
+ remove(id: number): Promise<number>;
26
+ /** Out of the bin, with whatever went in with it. Answers how many came back. */
27
+ restore(id: number): Promise<number>;
28
+ /** The publications, newest first. */
29
+ versions(id: number): Promise<PageVersion[]>;
30
+ /** An old publication becomes the draft; putting it on the site is a separate step. */
31
+ restoreVersion(id: number, number: number): Promise<PageDetail>;
32
+ }
33
+ /** Everything under `/pages`, below the panel's API path. */
34
+ export declare function createPagesApi(admin: AdminContext): PagesApi;
@@ -0,0 +1,43 @@
1
+ import { InjectionKey, Ref } from 'vue';
2
+ import { ScreenModel } from '@webx-ui/schema';
3
+ import { PageRow } from './types';
4
+ /**
5
+ * What the editor knows and the nodes of its screen do not.
6
+ *
7
+ * The form is a described screen (`pages.form`), so the parts of it that are not fields —
8
+ * where the page sits, what may be done to it, what its history is — arrive as node types of
9
+ * their own. Each of those needs the page being edited, and none of them can be handed it
10
+ * through the description: a screen is a description, not a binding. So the page that hosts
11
+ * the screen provides it, the same way it provides the preview to `wx-blocks`.
12
+ */
13
+ export interface PageEditorContext {
14
+ /** The page as the server last answered it, or `null` while the first request is in flight. */
15
+ page: Ref<PageRow | null>;
16
+ /** The trail above it, home page first. */
17
+ ancestors: Ref<PageRow[]>;
18
+ /**
19
+ * The values of the screen as they are right now, edits included.
20
+ *
21
+ * A node that only draws still has to follow what is being typed: the address is a sentence
22
+ * made of the slug field, and one that updated on save would be showing the old address at
23
+ * the moment the new one is most worth reading.
24
+ */
25
+ values: Ref<ScreenModel>;
26
+ /** The address of the page above, by content language. */
27
+ prefixes: Ref<Record<string, string>>;
28
+ /** Where the section lives, for the links the nodes draw. */
29
+ base: string;
30
+ /** Whether this administrator may write at all. */
31
+ canManage: boolean;
32
+ /** Ask the server for the page again — after a move, a restore, a publication. */
33
+ reload(): Promise<void>;
34
+ /** Write the draft now rather than at the end of the next pause. */
35
+ save(): Promise<void>;
36
+ }
37
+ export declare const pageEditorKey: InjectionKey<PageEditorContext>;
38
+ export declare function providePageEditor(editor: PageEditorContext): void;
39
+ /**
40
+ * The editor above this node, or `null` outside one — a node of `pages.form` drawn in a demo
41
+ * or a test has no page to talk about, and must draw nothing rather than throw.
42
+ */
43
+ export declare function usePageEditor(): PageEditorContext | null;
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 usePagesMessages(): void;
@@ -0,0 +1,12 @@
1
+ export { pages, type PagesOptions } from './module';
2
+ export { createPagesApi, type PagesApi } from './api';
3
+ export { pageEditorKey, providePageEditor, usePageEditor, type PageEditorContext } from './editor';
4
+ export { pagesMessages } from './messages';
5
+ export { slugify } from './slug';
6
+ export { default as WxPagesPage } from './PagesPage';
7
+ export { default as WxPageEditorPage } from './PageEditorPage';
8
+ export { default as WxPagePicker } from './PagePicker';
9
+ export { default as WxPagePlace } from './PagePlace';
10
+ export { default as WxPageDanger } from './PageDanger';
11
+ export { default as WxPageHistory } from './PageHistory';
12
+ export type { PageCapabilities, PageConflict, PageDetail, PageDropZone, PageInput, PageLevel, PageMoveResult, PageQuery, PageRow, PageSave, PageStatus, PageVersion, } from './types';