@webx-ui/module-admin 0.4.2 → 0.5.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,16 @@ declare function __VLS_template(): {
2
2
  attrs: Partial<{}>;
3
3
  slots: {
4
4
  brand?(_: {}): any;
5
+ brand?(_: {}): any;
6
+ user?(_: {}): any;
7
+ user?(_: {}): any;
5
8
  user?(_: {}): any;
6
9
  nav?(_: {
7
10
  collapsed: boolean;
8
11
  }): any;
9
12
  nav?(_: {
10
13
  collapsed: boolean;
11
- onSelect: () => void;
14
+ select: () => void;
12
15
  }): any;
13
16
  };
14
17
  refs: {
@@ -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,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,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;
@@ -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,14 @@ 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 { createDates, useDates, type DateLike, type Dates } from './dates';
6
+ export { errorText, useErrorText } from './errors';
5
7
  export { createHttp, readCookie, HttpError, type Http, type HttpOptions, type RequestOptions, } from './http';
6
- export type { AdminModule, AdminStatus, AdminUser, Manifest, ManifestModule, NavEntry, } from './types';
8
+ export { adminTypes } from './screenTypes';
9
+ export type { AdminModule, AdminStatus, AdminUser, Manifest, ManifestModule, NavEntry, RowAction, } from './types';
7
10
  export { default as AdminShell } from './AdminShell';
8
11
  export { default as AdminNav } from './AdminNav';
9
12
  export { default as WxScreen } from './Screen';
13
+ export { default as WxListScreen } from './ListScreen';
14
+ export { default as WxRowMenu } from './RowMenu';
15
+ export { default as WxDate } from './DateText';