@webx-ui/module-admin 0.4.2 → 0.6.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/README.md CHANGED
@@ -76,6 +76,38 @@ behind it still has labels.
76
76
  That is what an editing screen builds its tabs from, and it has nothing to do with the language
77
77
  of the interface around them.
78
78
 
79
+ ## Dates
80
+
81
+ One way to say when something happened, for every screen in the panel.
82
+
83
+ ```vue
84
+ <script setup lang="ts">
85
+ import { useDates, WxDate } from '@webx-ui/module-admin'
86
+
87
+ const dates = useDates() // dates.short(row.updated_at), dates.exact(…), dates.iso(…)
88
+ </script>
89
+
90
+ <template>
91
+ <wx-date :value="row.last_login_at" />
92
+ </template>
93
+ ```
94
+
95
+ | When | What it says |
96
+ | ------------ | ----------------------- |
97
+ | today | `today at 08:10` |
98
+ | yesterday | `yesterday at 14:03` |
99
+ | this year | `16 September at 14:03` |
100
+ | earlier | `16 September 2025` |
101
+ | never at all | `never` |
102
+
103
+ Twenty-four hours, no seconds; `WxDate` keeps the exact moment in a tip and in `<time
104
+ datetime>`, so a column is read at a glance and an argument about the exact second still has an
105
+ answer. The month names and the order of the parts come from `Intl` in the **panel's** language,
106
+ not the browser's; `today` is counted in the reader's own day, which is why a server sending UTC
107
+ does not turn somebody's morning into yesterday.
108
+
109
+ Nothing here touches ordering: a sortable column sorts on the value the server sent.
110
+
79
111
  ## Talking to the backend
80
112
 
81
113
  ```ts
@@ -111,6 +143,7 @@ everything, the same rule the server applies.
111
143
  | `useAdmin` | The context: `http`, `state`, `nav`, `can` |
112
144
  | `createHttp`, `HttpError` | The client, usable on its own |
113
145
  | `AdminShell`, `AdminNav` | The layout, for a panel that assembles its own |
146
+ | `useDates`, `WxDate` | When something happened, in words |
114
147
 
115
148
  Styles come with `@webx-ui/core`; this package adds a little of its own:
116
149
 
@@ -2,13 +2,22 @@ declare function __VLS_template(): {
2
2
  attrs: Partial<{}>;
3
3
  slots: {
4
4
  brand?(_: {}): any;
5
- user?(_: {}): any;
5
+ brand?(_: {}): any;
6
+ user?(_: {
7
+ expanded: boolean;
8
+ }): any;
9
+ user?(_: {
10
+ expanded: boolean;
11
+ }): any;
12
+ user?(_: {
13
+ expanded: boolean;
14
+ }): any;
6
15
  nav?(_: {
7
16
  collapsed: boolean;
8
17
  }): any;
9
18
  nav?(_: {
10
19
  collapsed: boolean;
11
- onSelect: () => void;
20
+ select: () => void;
12
21
  }): any;
13
22
  };
14
23
  refs: {
@@ -0,0 +1,25 @@
1
+ import { ActionSize } from '@webx-ui/core';
2
+ import { RouteLocationRaw } from 'vue-router';
3
+ /**
4
+ * The way out of an editor, said plainly.
5
+ *
6
+ * A trail of breadcrumbs is where the reader *is*; it is not a way out, and on a phone it is
7
+ * four words in the smallest type on the screen. Every screen that opens one record — a page,
8
+ * a block, a rule — ends up needing the same control, so it is one control rather than each
9
+ * editor's own arrow.
10
+ *
11
+ * An arrow and nothing else: the label is the tooltip, because the name of the section it
12
+ * returns to is already written next to it in the trail, and twice is noise.
13
+ */
14
+ type __VLS_Props = {
15
+ /** Where it leads — the section's list, normally. */
16
+ to: RouteLocationRaw;
17
+ /** What the tooltip says. Defaults to the panel's own word for it. */
18
+ label?: string;
19
+ size?: ActionSize;
20
+ };
21
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
22
+ size: ActionSize;
23
+ label: string;
24
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
25
+ export default _default;
@@ -0,0 +1,23 @@
1
+ import { TextSize, TextTone } from '@webx-ui/core';
2
+ import { DateLike } from './dates';
3
+ /**
4
+ * When something happened, the way the whole panel says it (§21).
5
+ *
6
+ * The short line is what the column holds — "today at 08:10" is read at a glance where
7
+ * "17.09.2026, 08:10" is read twice. The second down to the second stays in the tip, because
8
+ * the question "when exactly" is asked rarely and answered badly by a column.
9
+ *
10
+ * Its own tip rather than `title`: the native one waits a second, cannot be styled, and is
11
+ * invisible to anybody who is not holding a mouse (§16).
12
+ */
13
+ type __VLS_Props = {
14
+ /** What the server sent. `null` is a date that never happened, not a missing one. */
15
+ value: DateLike;
16
+ size?: TextSize;
17
+ tone?: TextTone;
18
+ };
19
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
20
+ size: TextSize;
21
+ tone: TextTone;
22
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
23
+ export default _default;
@@ -0,0 +1,31 @@
1
+ import { ActionSize } from '@webx-ui/core';
2
+ /**
3
+ * A `?` beside something that needs explaining, and the page of explanation behind it.
4
+ *
5
+ * The text is Markdown and comes from wherever the caller keeps it — normally a `help` group
6
+ * in a module's `lang` files, which means it is translated in the same place as everything
7
+ * else and is already on the client with the rest of the dictionary. No request, no second
8
+ * store, and a language nobody has translated falls back to the site's own the way every
9
+ * other line does.
10
+ *
11
+ * Keeping it as Markdown rather than as a screen of components is what lets the same words
12
+ * serve an agent: a module can hand the identical text to MCP as `text/markdown`, and then
13
+ * there is one explanation of a thing rather than two that drift.
14
+ */
15
+ type __VLS_Props = {
16
+ /** The page, in Markdown. */
17
+ body: string;
18
+ /** Heading of the dialog. Defaults to the panel's own word for it. */
19
+ title?: string;
20
+ /** Tooltip of the button. Defaults to the heading. */
21
+ label?: string;
22
+ size?: ActionSize;
23
+ width?: number | string;
24
+ };
25
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
26
+ title: string;
27
+ size: ActionSize;
28
+ label: string;
29
+ width: number | string;
30
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
31
+ export default _default;
@@ -0,0 +1,80 @@
1
+ import { CardPadding, TabItem, TabValue } from '@webx-ui/core';
2
+ /**
3
+ * The frame every list of the panel is drawn in.
4
+ *
5
+ * One shape for all of them, because five sections that each answered "where does the heading
6
+ * go" on their own gave five answers — a heading inside the card on `Administrators`, two
7
+ * headings on `SEO`, a search outside the card on `Blocks`, no heading at all on `Files`. A
8
+ * reader who learns one list should already know the next.
9
+ *
10
+ * The shape is: the section's name on the left of its own line, the one action the section
11
+ * exists for on the right of it, the views of the list as tabs under that, and a card holding
12
+ * nothing but the rows. The card has no heading of its own — the tab says which view it is,
13
+ * and the line above says which section (§19). The search stays inside the table, where it is
14
+ * a property of the rows rather than of the screen (§10).
15
+ *
16
+ * What it deliberately does not do is fetch, filter or page. It is a frame; the section still
17
+ * owns its data, and the tabs only say which view is open.
18
+ */
19
+ type __VLS_Props = {
20
+ /** The section's name. The one heading on the screen. */
21
+ title?: string;
22
+ /**
23
+ * The views of the same list — "All / Drafts / Published", "Rules / Redirects". Not a
24
+ * navigation between screens: everything under them is the same table.
25
+ */
26
+ views?: TabItem[];
27
+ /** Wrap the rows in a card. Off for a section that brings its own. */
28
+ card?: boolean;
29
+ /** Padding of that card; `WxCard`'s own default when not given. */
30
+ padding?: CardPadding;
31
+ /** The screen is as tall as the column it is drawn in — a file manager, a board. */
32
+ fill?: boolean;
33
+ /** Container width below which the views fold into one switch. */
34
+ collapseBelow?: number;
35
+ };
36
+ type __VLS_PublicProps = {
37
+ 'view'?: TabValue | undefined;
38
+ } & __VLS_Props;
39
+ declare function __VLS_template(): {
40
+ attrs: Partial<{}>;
41
+ slots: Readonly<{
42
+ /** The rows: a table, a grid of cards, a manager. */
43
+ default?: () => unknown;
44
+ /**
45
+ * The screen's buttons, beside the heading. One of them — the reason the section exists —
46
+ * is a filled primary button with a word on it; everything else is outlined or an icon.
47
+ */
48
+ actions?: () => unknown;
49
+ }> & {
50
+ /** The rows: a table, a grid of cards, a manager. */
51
+ default?: () => unknown;
52
+ /**
53
+ * The screen's buttons, beside the heading. One of them — the reason the section exists —
54
+ * is a filled primary button with a word on it; everything else is outlined or an icon.
55
+ */
56
+ actions?: () => unknown;
57
+ };
58
+ refs: {};
59
+ rootEl: HTMLDivElement;
60
+ };
61
+ type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
62
+ declare const __VLS_component: import('vue').DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
63
+ "update:view": (value: TabValue | undefined) => any;
64
+ }, string, import('vue').PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
65
+ "onUpdate:view"?: ((value: TabValue | undefined) => any) | undefined;
66
+ }>, {
67
+ title: string;
68
+ views: TabItem[];
69
+ card: boolean;
70
+ padding: CardPadding;
71
+ fill: boolean;
72
+ collapseBelow: number;
73
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
74
+ declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
75
+ export default _default;
76
+ type __VLS_WithTemplateSlots<T, S> = T & {
77
+ new (): {
78
+ $slots: S;
79
+ };
80
+ };
@@ -0,0 +1,163 @@
1
+ import { nextTick } from 'vue';
2
+ import { ActionSize } from '@webx-ui/core';
3
+ /**
4
+ * Renaming, made an act rather than a side effect.
5
+ *
6
+ * A heading that is quietly an `<input>` reads as a heading: nothing says it can be typed in,
7
+ * and the reader finds out by accident — or never. Worse, it is bound straight to the record,
8
+ * so a stray keystroke on what looks like a title is a rename nobody asked for.
9
+ *
10
+ * So the name is text, with a pencil beside it. The pencil opens a small form: one field, one
11
+ * button, and nothing changes until that button is pressed. Escape and a click outside leave
12
+ * the name as it was.
13
+ */
14
+ type __VLS_Props = {
15
+ /** What is being renamed. Only written back when the form is submitted. */
16
+ name?: string;
17
+ /** Heading of the form, and the pencil's tooltip. Defaults to the panel's own word. */
18
+ label?: string;
19
+ placeholder?: string;
20
+ disabled?: boolean;
21
+ size?: ActionSize;
22
+ };
23
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
24
+ rename: (name: string) => any;
25
+ }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
26
+ onRename?: ((name: string) => any) | undefined;
27
+ }>, {
28
+ size: ActionSize;
29
+ label: string;
30
+ placeholder: string;
31
+ name: string;
32
+ disabled: boolean;
33
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
34
+ field: ({
35
+ $: import('vue').ComponentInternalInstance;
36
+ $data: {};
37
+ $props: {
38
+ readonly modelValue?: import('@webx-ui/core').InputModelValue;
39
+ readonly id?: string | undefined;
40
+ readonly type?: import('@webx-ui/core').InputNativeType | undefined;
41
+ readonly size?: import('@webx-ui/core').InputSize | undefined;
42
+ readonly status?: import('@webx-ui/core').InputStatus | undefined;
43
+ readonly placeholder?: string | undefined;
44
+ readonly disabled?: boolean | undefined;
45
+ readonly readonly?: boolean | undefined;
46
+ readonly clearable?: boolean | undefined;
47
+ readonly maxlength?: number | undefined;
48
+ readonly showCount?: boolean | undefined;
49
+ readonly autocomplete?: string | undefined;
50
+ readonly ariaLabel?: string | undefined;
51
+ readonly localized?: boolean | undefined;
52
+ readonly onChange?: ((value: string) => any) | undefined | undefined;
53
+ readonly "onUpdate:modelValue"?: ((value: import('@webx-ui/core').InputModelValue) => any) | undefined | undefined;
54
+ readonly onInput?: ((value: string) => any) | undefined | undefined;
55
+ readonly onClear?: (() => any) | undefined | undefined;
56
+ readonly onBlur?: ((event: FocusEvent) => any) | undefined | undefined;
57
+ readonly onFocus?: ((event: FocusEvent) => any) | undefined | undefined;
58
+ } & import('vue').VNodeProps & import('vue').AllowedComponentProps & import('vue').ComponentCustomProps;
59
+ $attrs: import('vue').Attrs;
60
+ $refs: {
61
+ [x: string]: unknown;
62
+ } & {
63
+ inputRef: HTMLInputElement;
64
+ };
65
+ $slots: Readonly<{
66
+ [name: string]: import('vue').Slot<any> | undefined;
67
+ }>;
68
+ $root: import('vue').ComponentPublicInstance | null;
69
+ $parent: import('vue').ComponentPublicInstance | null;
70
+ $host: Element | null;
71
+ $emit: ((event: "input", value: string) => void) & ((event: "clear") => void) & ((event: "blur", event: FocusEvent) => void) & ((event: "change", value: string) => void) & ((event: "focus", event: FocusEvent) => void) & ((event: "update:modelValue", value: import('@webx-ui/core').InputModelValue) => void);
72
+ $el: any;
73
+ $options: import('vue').ComponentOptionsBase<Readonly<{
74
+ modelValue?: import('@webx-ui/core').InputModelValue;
75
+ } & import('@webx-ui/core').InputProps> & Readonly<{
76
+ onChange?: ((value: string) => any) | undefined;
77
+ "onUpdate:modelValue"?: ((value: import('@webx-ui/core').InputModelValue) => any) | undefined;
78
+ onInput?: ((value: string) => any) | undefined;
79
+ onClear?: (() => any) | undefined;
80
+ onBlur?: ((event: FocusEvent) => any) | undefined;
81
+ onFocus?: ((event: FocusEvent) => any) | undefined;
82
+ }>, {
83
+ focus: () => void | undefined;
84
+ blur: () => void | undefined;
85
+ select: () => void | undefined;
86
+ input: import('vue').Ref<HTMLInputElement | null, HTMLInputElement | null>;
87
+ }, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
88
+ change: (value: string) => any;
89
+ "update:modelValue": (value: import('@webx-ui/core').InputModelValue) => any;
90
+ input: (value: string) => any;
91
+ clear: () => any;
92
+ blur: (event: FocusEvent) => any;
93
+ focus: (event: FocusEvent) => any;
94
+ }, string, {
95
+ size: import('@webx-ui/core').InputSize;
96
+ disabled: boolean;
97
+ type: import('@webx-ui/core').InputNativeType;
98
+ placeholder: string;
99
+ ariaLabel: string;
100
+ clearable: boolean;
101
+ status: import('@webx-ui/core').InputStatus;
102
+ id: string;
103
+ readonly: boolean;
104
+ autocomplete: string;
105
+ maxlength: number;
106
+ showCount: boolean;
107
+ localized: boolean;
108
+ }, {}, string, {}, import('vue').GlobalComponents, import('vue').GlobalDirectives, string, import('vue').ComponentProvideOptions> & {
109
+ beforeCreate?: (() => void) | (() => void)[];
110
+ created?: (() => void) | (() => void)[];
111
+ beforeMount?: (() => void) | (() => void)[];
112
+ mounted?: (() => void) | (() => void)[];
113
+ beforeUpdate?: (() => void) | (() => void)[];
114
+ updated?: (() => void) | (() => void)[];
115
+ activated?: (() => void) | (() => void)[];
116
+ deactivated?: (() => void) | (() => void)[];
117
+ beforeDestroy?: (() => void) | (() => void)[];
118
+ beforeUnmount?: (() => void) | (() => void)[];
119
+ destroyed?: (() => void) | (() => void)[];
120
+ unmounted?: (() => void) | (() => void)[];
121
+ renderTracked?: ((e: import('vue').DebuggerEvent) => void) | ((e: import('vue').DebuggerEvent) => void)[];
122
+ renderTriggered?: ((e: import('vue').DebuggerEvent) => void) | ((e: import('vue').DebuggerEvent) => void)[];
123
+ errorCaptured?: ((err: unknown, instance: import('vue').ComponentPublicInstance | null, info: string) => boolean | void) | ((err: unknown, instance: import('vue').ComponentPublicInstance | null, info: string) => boolean | void)[];
124
+ };
125
+ $forceUpdate: () => void;
126
+ $nextTick: typeof nextTick;
127
+ $watch<T extends string | ((...args: any) => any)>(source: T, cb: T extends (...args: any) => infer R ? (...args: [R, R, import('@vue/reactivity').OnCleanup]) => any : (...args: [any, any, import('@vue/reactivity').OnCleanup]) => any, options?: import('vue').WatchOptions): import('vue').WatchStopHandle;
128
+ } & Readonly<{
129
+ size: import('@webx-ui/core').InputSize;
130
+ disabled: boolean;
131
+ type: import('@webx-ui/core').InputNativeType;
132
+ placeholder: string;
133
+ ariaLabel: string;
134
+ clearable: boolean;
135
+ status: import('@webx-ui/core').InputStatus;
136
+ id: string;
137
+ readonly: boolean;
138
+ autocomplete: string;
139
+ maxlength: number;
140
+ showCount: boolean;
141
+ localized: boolean;
142
+ }> & Omit<Readonly<{
143
+ modelValue?: import('@webx-ui/core').InputModelValue;
144
+ } & import('@webx-ui/core').InputProps> & Readonly<{
145
+ onChange?: ((value: string) => any) | undefined;
146
+ "onUpdate:modelValue"?: ((value: import('@webx-ui/core').InputModelValue) => any) | undefined;
147
+ onInput?: ((value: string) => any) | undefined;
148
+ onClear?: (() => any) | undefined;
149
+ onBlur?: ((event: FocusEvent) => any) | undefined;
150
+ onFocus?: ((event: FocusEvent) => any) | undefined;
151
+ }>, "type" | "size" | "ariaLabel" | "input" | "select" | "placeholder" | "id" | "status" | "localized" | "blur" | "focus" | "disabled" | "readonly" | "clearable" | "maxlength" | "showCount" | "autocomplete"> & {
152
+ focus: () => void | undefined;
153
+ blur: () => void | undefined;
154
+ select: () => void | undefined;
155
+ input: HTMLInputElement | null;
156
+ } & {} & import('vue').ComponentCustomProperties & {} & {
157
+ $slots: {
158
+ prefix?(_: {}): any;
159
+ suffix?(_: {}): any;
160
+ };
161
+ }) | null;
162
+ }, any>;
163
+ export default _default;
@@ -0,0 +1,28 @@
1
+ import { ActionSize } from '@webx-ui/core';
2
+ import { RowAction } from './types';
3
+ /**
4
+ * What one record offers, in every list of the panel: a `···` at the end of the row.
5
+ *
6
+ * Always a menu, even for a single action. The panel used to have three answers to this —
7
+ * a menu on `Pages`, a bare red bin on `SEO`, a toolbar over the selection in the library —
8
+ * and the cost of that is not the extra click, it is that nobody knows where to look. A
9
+ * menu also has room for the word: an unlabelled icon says what it does only to whoever has
10
+ * already learned it (§20).
11
+ *
12
+ * Two rules it keeps for every list rather than leaving to each one. The destructive action
13
+ * goes last, behind a rule, in red — it is never the thing the reader meant to hit. And what
14
+ * somebody may not do is left out, not shown greyed: a menu is a list of what is possible,
15
+ * and a row of dead entries teaches nothing.
16
+ */
17
+ type __VLS_Props = {
18
+ /** What may be done, in the order it should read. */
19
+ actions: RowAction[];
20
+ /** Accessible name of the menu — the record it belongs to. */
21
+ label?: string;
22
+ size?: ActionSize;
23
+ };
24
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
25
+ size: ActionSize;
26
+ label: string;
27
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
28
+ export default _default;
package/dist/admin.d.ts CHANGED
@@ -26,6 +26,15 @@ export interface AdminContext {
26
26
  readonly types: TypeRegistry;
27
27
  /** Ask the server what the panel is and who is signed in again. */
28
28
  reload(): Promise<void>;
29
+ /**
30
+ * Fetch the manifest again and adopt it, without the panel going through `loading` — so the
31
+ * chrome changes under whoever is looking at it instead of being replaced by a spinner.
32
+ *
33
+ * For a screen that has just saved something the manifest reports: the branding, the name of
34
+ * the site, what a section is called. A failure is swallowed, because what it would cost is
35
+ * a working panel in exchange for a stale logo.
36
+ */
37
+ refreshManifest(): Promise<void>;
29
38
  /**
30
39
  * Draw the panel in another language: fetches that dictionary and remembers the choice for
31
40
  * the next visit. Storing it against the administrator is an auth module's business — this
@@ -0,0 +1,40 @@
1
+ import { I18n } from './i18n';
2
+ /** A moment as it arrives: ISO-8601 from the server, a timestamp, a `Date`, or nothing. */
3
+ export type DateLike = string | number | Date | null | undefined;
4
+ export interface Dates {
5
+ /**
6
+ * The line a reader sees: "today at 08:10", "yesterday at 14:03", "16 September at 14:03",
7
+ * "16 September 2025", "never".
8
+ */
9
+ short(value: DateLike): string;
10
+ /**
11
+ * Everything there is, down to the second — what the tip holds when the short line is not
12
+ * precise enough to settle an argument. Empty when there is no date.
13
+ */
14
+ exact(value: DateLike): string;
15
+ /** The machine value for `<time datetime>`. Empty when there is no date. */
16
+ iso(value: DateLike): string;
17
+ }
18
+ /**
19
+ * How the panel says when something happened — one answer for every screen in it.
20
+ *
21
+ * Three rules it would be easy to lose, and each of them was a bug before this existed:
22
+ *
23
+ * - the language is the panel's, not the browser's. `toLocaleString()` with no locale reads
24
+ * the browser's settings, so a panel drawn in Russian was dating its rows in American
25
+ * order. The locale comes from `i18n`, which is what the administrator chose.
26
+ * - "today" is counted in the administrator's own day. The server sends UTC; comparing UTC
27
+ * dates would write "yesterday" over anything that happened before 03:00 in Moscow. The
28
+ * comparison is made on local calendar days, which is what the reader means by today.
29
+ * - the shown string is never what sorts. Nothing here touches ordering: a column sorts on
30
+ * the value the server sent, or "yesterday" lands between the sixteenth and the seventeenth.
31
+ */
32
+ export declare function createDates(i18n: I18n): Dates;
33
+ /**
34
+ * The formatter, inside a panel or outside one.
35
+ *
36
+ * Outside a panel — a screen mounted by hand, a story, a test — there is no dictionary to read,
37
+ * so one is made once with this package's own English in it. A component that may be used
38
+ * either way should not have to know which it is.
39
+ */
40
+ export declare function useDates(): Dates;
@@ -0,0 +1,24 @@
1
+ import { Translate } from './i18n';
2
+ /**
3
+ * What to say out loud when a request came back a failure.
4
+ *
5
+ * The panel used to print whatever the server put in `message`, which is fine right up until
6
+ * the server has not written it. `No query results for model [WebxUi\Pages\Models\Page] 8` is
7
+ * Laravel talking to a developer — a namespace, in English, in a panel translated into ten
8
+ * languages — and it reached an editor who had only clicked a row in the bin (§13.3).
9
+ *
10
+ * So the rule is which status the panel trusts to speak for itself, not which words look
11
+ * technical. A 422 is ours: every refusal that reaches one is written by a module to be read,
12
+ * through `trans()`, and the validator's lines are translated by Laravel. Everything else —
13
+ * 404, 403, 500, a server that never answered — carries text nobody wrote for this reader, and
14
+ * the panel says it in its own words instead.
15
+ */
16
+ export declare function errorText(error: unknown, t: Translate, fallback?: string): string;
17
+ /**
18
+ * The same thing for a component, bound to the panel's own dictionary.
19
+ *
20
+ * A module calls this rather than its own `t`, because these lines belong to the panel: every
21
+ * section fails in the same handful of ways, and translating "you are not allowed to do that"
22
+ * once per module is how nine of them end up saying it differently.
23
+ */
24
+ export declare function useErrorText(): (error: unknown, fallback?: string) => string;
package/dist/index.d.ts CHANGED
@@ -2,8 +2,18 @@ export { createAdmin, type Admin, type AdminPlugin, type CreateAdminOptions } fr
2
2
  export { createAdminContext, provideAdmin, useAdmin, adminKey, type AdminContext, type AdminState, } from './admin';
3
3
  export { createI18n, provideI18n, useI18n, useTranslate, i18nKey, type Dictionary, type I18n, type I18nState, type LocaleDescriptor, type Messages, type Translate, } from './i18n';
4
4
  export { adminMessages } from './messages';
5
+ export { renderMarkdown } from './markdown';
6
+ export { createDates, useDates, type DateLike, type Dates } from './dates';
7
+ export { errorText, useErrorText } from './errors';
5
8
  export { createHttp, readCookie, HttpError, type Http, type HttpOptions, type RequestOptions, } from './http';
6
- export type { AdminModule, AdminStatus, AdminUser, Manifest, ManifestModule, NavEntry, } from './types';
9
+ export { adminTypes } from './screenTypes';
10
+ export type { AdminModule, AdminStatus, AdminUser, Manifest, ManifestModule, NavEntry, RowAction, } from './types';
7
11
  export { default as AdminShell } from './AdminShell';
8
12
  export { default as AdminNav } from './AdminNav';
9
13
  export { default as WxScreen } from './Screen';
14
+ export { default as WxListScreen } from './ListScreen';
15
+ export { default as WxRowMenu } from './RowMenu';
16
+ export { default as WxBackButton } from './BackButton';
17
+ export { default as WxRenameButton } from './RenameButton';
18
+ export { default as WxHelpButton } from './HelpButton';
19
+ export { default as WxDate } from './DateText';