@webx-ui/module-inbox 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.
@@ -0,0 +1,15 @@
1
+ import { Ref, WritableComputedRef } from 'vue';
2
+ import { FormOptions } from './types';
3
+ /**
4
+ * One setting of a form, as something a control can be bound to.
5
+ *
6
+ * The keys are literal and several of them have dots in them — `thank-you.heading` is one key
7
+ * (§5) — so nothing here splits on a dot, and a whole new object is written on every change
8
+ * rather than a property being set in place: the editor holds the options as one value, and a
9
+ * mutation underneath it is a change nothing is told about.
10
+ *
11
+ * An empty value is not written as an empty value: it is taken out. The server does the same
12
+ * when it saves, and a form whose settings are twelve blank strings reads as configured when
13
+ * it is not.
14
+ */
15
+ export declare function option<T>(options: Ref<FormOptions>, key: string, fallback: T): WritableComputedRef<T>;
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .wx-inbox-field-form[data-v-cd153059]{display:flex;flex-direction:column;gap:var(--wx-space-12)}.wx-inbox-field-form__row[data-v-cd153059]{display:flex;flex-wrap:wrap;gap:var(--wx-space-12)}.wx-inbox-field-form__row[data-v-cd153059]>*{flex:1 1 200px;min-width:0}.wx-inbox-field-form__row[data-v-cd153059]:empty{display:none}.wx-inbox-field-form__checks[data-v-cd153059]{display:flex;flex-wrap:wrap;gap:var(--wx-space-16)}.wx-inbox-choices[data-v-cd153059]{display:flex;flex-direction:column;gap:var(--wx-space-8);align-items:flex-start}.wx-inbox-choices__row[data-v-cd153059]{display:flex;align-items:center;gap:var(--wx-space-8);width:100%}.wx-inbox-choices__row[data-v-cd153059]>:first-child{flex:0 1 180px;min-width:0}.wx-inbox-choices__row[data-v-cd153059]>:nth-child(2){flex:1 1 auto;min-width:0}.wx-inbox-field[data-v-3b684f07]{display:flex;flex-direction:column;gap:var(--wx-space-2);flex:1 1 auto;min-width:0}.wx-inbox-field__name[data-v-3b684f07]{display:flex;align-items:center;gap:var(--wx-space-6);min-width:0}.wx-inbox-embed[data-v-fdabb537]{display:flex;flex-direction:column;gap:var(--wx-space-16)}.wx-inbox-embed__snippet[data-v-fdabb537]{display:flex;align-items:center;gap:var(--wx-space-8);margin-top:var(--wx-space-12);padding:var(--wx-space-10) var(--wx-space-12);border:1px solid var(--wx-border-default);border-radius:var(--wx-radius-md);background:var(--wx-bg-subtle)}.wx-inbox-embed__snippet code[data-v-fdabb537]{flex:1 1 auto;min-width:0;overflow-x:auto;white-space:pre;font-family:var(--wx-font-family-mono);font-size:var(--wx-font-size-sm)}.wx-inbox-general[data-v-35e56e3d]{display:flex;flex-direction:column;gap:var(--wx-space-16)}.wx-inbox-recipients[data-v-48ebf651]{display:flex;flex-direction:column;gap:var(--wx-space-8);margin-block:var(--wx-space-12)}.wx-inbox-recipients__row[data-v-48ebf651]{display:flex;align-items:center;gap:var(--wx-space-8)}.wx-inbox-recipients__row[data-v-48ebf651]>:first-child{flex:1 1 auto;min-width:0}.wx-inbox-editor[data-v-194b4a47]{display:flex;flex-direction:column;gap:var(--wx-space-16);min-height:0}.wx-inbox-editor__head[data-v-194b4a47]{display:flex;align-items:center;gap:var(--wx-space-12);flex-wrap:wrap}.wx-inbox-editor__id[data-v-194b4a47]{min-width:0}.wx-inbox-editor__title[data-v-194b4a47]{margin:0;font-size:var(--wx-font-size-xl);font-weight:var(--wx-font-weight-semibold);line-height:var(--wx-font-line-height-tight)}.wx-inbox-editor__tabs[data-v-194b4a47]{flex:1 1 auto;min-height:0}.wx-inbox-create[data-v-55fb40d8],.wx-inbox-by-hand[data-v-72006299]{display:flex;flex-direction:column;gap:var(--wx-space-12)}.wx-submissions[data-v-7ef0e131]{display:flex;flex-direction:column;gap:var(--wx-space-12);padding:var(--wx-space-16);min-width:0;height:100%;min-height:0}.wx-submissions__head[data-v-7ef0e131]{display:flex;align-items:center;gap:var(--wx-space-8);flex-wrap:wrap}.wx-submissions__who[data-v-7ef0e131]{flex:1 1 auto;min-width:0}.wx-submissions__views[data-v-7ef0e131]{flex:1 1 auto;min-height:0}.wx-submissions__mass[data-v-7ef0e131]{display:flex;align-items:center;gap:var(--wx-space-8);flex-wrap:wrap}.wx-submissions[data-v-7ef0e131] .wx-submissions__when{white-space:nowrap}.wx-submissions__state[data-v-7ef0e131]{display:flex;align-items:center;flex-wrap:wrap;gap:var(--wx-space-6);min-width:0}.wx-submissions[data-v-7ef0e131] .is-unread{font-weight:var(--wx-font-weight-medium)}.wx-submissions[data-v-7ef0e131] .is-unread td:first-child{box-shadow:inset 2px 0 0 0 var(--wx-color-primary)}.wx-inbox{height:100%;min-height:0}.wx-inbox>.wx-card__body{height:100%;min-height:0;display:flex;flex-direction:column}.wx-inbox__panes{flex:1 1 auto;min-height:0}.wx-inbox__loading{padding:var(--wx-space-16)}.wx-inbox__forms{padding:var(--wx-space-8)}.wx-inbox-form{display:flex;flex-direction:column;gap:var(--wx-space-2);align-items:flex-start;flex:1 1 auto;min-width:0;padding:0;border:0;background:none;font:inherit;color:inherit;text-align:start;cursor:pointer}.wx-inbox-form__name{display:flex;align-items:center;gap:var(--wx-space-6);min-width:0;max-width:100%}.wx-inbox__forms .wx-sortable-list__row:has(.wx-inbox-form.is-current){background:var(--wx-bg-subtle);border-radius:var(--wx-radius-sm)}.wx-inbox-form.is-current{color:var(--wx-color-primary)}.wx-inbox-form__count{flex:0 0 auto}.wx-inbox-status-form[data-v-b520b53b]{display:flex;flex-direction:column;gap:var(--wx-space-12)}.wx-inbox-status-form__checks[data-v-b520b53b]{display:flex;flex-wrap:wrap;gap:var(--wx-space-16)}.wx-inbox-statuses[data-v-8c5d0882]{display:flex;flex-direction:column;align-items:flex-start;gap:var(--wx-space-12)}.wx-inbox-statuses>.wx-list-screen[data-v-8c5d0882]{align-self:stretch}.wx-inbox-status[data-v-8c5d0882]{display:flex;align-items:center;gap:var(--wx-space-8);flex:1 1 auto;min-width:0;flex-wrap:wrap}.wx-submission[data-v-6b6abbf9]{display:flex;flex-direction:column;gap:var(--wx-space-16);min-width:0;container-type:inline-size}.wx-submission__head[data-v-6b6abbf9]{display:flex;align-items:center;gap:var(--wx-space-8);flex-wrap:wrap}.wx-submission__who[data-v-6b6abbf9]{flex:1 1 auto;min-width:0}.wx-submission__panes[data-v-6b6abbf9],.wx-submission__main[data-v-6b6abbf9],.wx-submission__aside[data-v-6b6abbf9]{display:flex;flex-direction:column;gap:var(--wx-space-16);min-width:0}@container (min-width: 900px){.wx-submission__panes[data-v-6b6abbf9]{flex-direction:row;align-items:flex-start}.wx-submission__main[data-v-6b6abbf9]{flex:1 1 auto}.wx-submission__aside[data-v-6b6abbf9]{flex:0 0 320px;max-width:320px}}.wx-submission__value[data-v-6b6abbf9]{margin:0;white-space:pre-wrap;overflow-wrap:anywhere}.wx-submission__detail[data-v-6b6abbf9]{overflow-wrap:anywhere}.wx-submission__event[data-v-6b6abbf9]{margin:0}.wx-submission__by[data-v-6b6abbf9]{display:flex;align-items:baseline;gap:var(--wx-space-6);flex-wrap:wrap;margin:0}.wx-submission__files[data-v-6b6abbf9]{display:flex;flex-wrap:wrap;gap:var(--wx-space-8)}.wx-submission__fields[data-v-6b6abbf9]{display:flex;flex-direction:column;gap:var(--wx-space-12)}.wx-submission__field[data-v-6b6abbf9]{display:flex;flex-direction:column;gap:var(--wx-space-4)}
@@ -0,0 +1,266 @@
1
+ import { LocalizedValue } from '@webx-ui/core';
2
+ /** The kinds of question a form can ask (§4). Mirrors `WebxUi\Inbox\Fields\FieldType`. */
3
+ export declare const FIELD_TYPES: readonly ["text", "email", "tel", "textarea", "select", "radio", "checkbox", "consent", "date", "file", "hidden"];
4
+ export type FieldType = (typeof FIELD_TYPES)[number];
5
+ /** Tones of `WxBadge`, which is what a status is coloured with — never a hex value. */
6
+ export declare const STATUS_COLORS: readonly ["default", "primary", "success", "warning", "info", "danger"];
7
+ export type StatusColor = (typeof STATUS_COLORS)[number];
8
+ /** One written-down answer of a `select`, `radio` or `checkbox`. */
9
+ export interface FieldChoice {
10
+ value: string;
11
+ label: LocalizedValue | string;
12
+ }
13
+ /**
14
+ * What a field may be configured with. Which keys mean anything depends on the type (§4), and
15
+ * the server keeps only the ones that do.
16
+ */
17
+ export interface FieldOptions {
18
+ maxlength?: number;
19
+ rows?: number;
20
+ pattern?: string;
21
+ min?: number | string;
22
+ max?: number | string;
23
+ choices?: FieldChoice[];
24
+ text?: LocalizedValue | string;
25
+ max_size?: number;
26
+ extensions?: string[];
27
+ multiple?: boolean;
28
+ [key: string]: unknown;
29
+ }
30
+ export interface InboxField {
31
+ id: number;
32
+ form_id: number;
33
+ /** What somebody typed, or null. */
34
+ name: string | null;
35
+ /** What the HTML will actually say — `f17` for a field nobody named. */
36
+ key: string;
37
+ type: FieldType;
38
+ title: LocalizedValue;
39
+ placeholder: LocalizedValue;
40
+ help: LocalizedValue;
41
+ options: FieldOptions;
42
+ is_enabled: boolean;
43
+ is_required: boolean;
44
+ is_fullsize: boolean;
45
+ in_table: boolean;
46
+ position: number;
47
+ [key: string]: unknown;
48
+ }
49
+ /** A field as the dialog saves it. */
50
+ export interface FieldInput {
51
+ name: string | null;
52
+ type: FieldType;
53
+ title: LocalizedValue;
54
+ placeholder?: LocalizedValue;
55
+ help?: LocalizedValue;
56
+ options?: FieldOptions;
57
+ is_enabled: boolean;
58
+ is_required: boolean;
59
+ is_fullsize: boolean;
60
+ in_table: boolean;
61
+ }
62
+ /** Who a notification goes to: somebody with an account, or an address typed in. */
63
+ export type Recipient = {
64
+ admin_id: number;
65
+ } | {
66
+ email: string;
67
+ };
68
+ /** The settings of a form (§5). Every key is literal, dots included. */
69
+ export interface FormOptions {
70
+ 'thank-you.heading'?: LocalizedValue;
71
+ 'thank-you.text'?: LocalizedValue;
72
+ 'design.submit-text'?: LocalizedValue;
73
+ redirect?: string;
74
+ recipients?: Recipient[];
75
+ email_field?: string;
76
+ 'antispam.honeypot'?: boolean;
77
+ 'antispam.min_seconds'?: number;
78
+ 'antispam.throttle'?: number;
79
+ 'antispam.captcha'?: 'off' | 'recaptcha' | 'turnstile';
80
+ [key: string]: unknown;
81
+ }
82
+ export interface InboxForm {
83
+ id: number;
84
+ slug: string;
85
+ title: LocalizedValue;
86
+ is_enabled: boolean;
87
+ options: FormOptions;
88
+ position: number;
89
+ /** Only where the query counted them — null on a form loaded on its own. */
90
+ submissions_count: number | null;
91
+ /** Unread and not spam. */
92
+ unread_count: number | null;
93
+ /** Only on a form asked for by id, or one just written. */
94
+ fields?: InboxField[];
95
+ created_at: string | null;
96
+ updated_at: string | null;
97
+ }
98
+ /** A form as its editor saves it. */
99
+ export interface FormInput {
100
+ slug: string;
101
+ title: LocalizedValue;
102
+ is_enabled: boolean;
103
+ options: FormOptions;
104
+ }
105
+ export interface InboxStatus {
106
+ id: number;
107
+ key: string;
108
+ title: LocalizedValue;
109
+ color: StatusColor;
110
+ is_default: boolean;
111
+ is_spam: boolean;
112
+ is_closed: boolean;
113
+ position: number;
114
+ submissions_count: number | null;
115
+ }
116
+ export interface StatusInput {
117
+ key: string;
118
+ title: LocalizedValue;
119
+ color: StatusColor;
120
+ is_default: boolean;
121
+ is_spam: boolean;
122
+ is_closed: boolean;
123
+ }
124
+ /** Somebody with an account who may read the section, for the recipients list. */
125
+ export interface InboxRecipient {
126
+ id: number;
127
+ name: string;
128
+ email: string;
129
+ }
130
+ /** An administrator as a name beside something else — an assignee, the author of a log line. */
131
+ export interface InboxAdmin {
132
+ id: number;
133
+ name: string;
134
+ email: string;
135
+ }
136
+ /** One column of the list: a field of the form that was marked `in_table`. */
137
+ export interface SubmissionColumn {
138
+ key: string;
139
+ label: string;
140
+ type: FieldType;
141
+ }
142
+ /** What each tab above the list would hold, under the filters that are on. */
143
+ export interface SubmissionCounts {
144
+ all: number;
145
+ unread: number;
146
+ /** By the status's key, spam included — its own tab has to say how much is in it. */
147
+ statuses: Record<string, number>;
148
+ }
149
+ /** A page as Laravel's `->paginate()` serialises it, which is what `WxTable` reads. */
150
+ export interface InboxPage<T> {
151
+ data: T[];
152
+ current_page: number;
153
+ last_page: number;
154
+ per_page: number;
155
+ total: number;
156
+ from: number | null;
157
+ to: number | null;
158
+ }
159
+ /** One line of the list. The answers are keyed by the field's machine name. */
160
+ export interface SubmissionRow {
161
+ id: number;
162
+ values: Record<string, string | null>;
163
+ status: InboxStatus | null;
164
+ assignee: InboxAdmin | null;
165
+ is_read: boolean;
166
+ source: string;
167
+ files_count: number;
168
+ created_at: string | null;
169
+ [key: string]: unknown;
170
+ }
171
+ /**
172
+ * The list, and the two things that travel with it: what its columns are, and what each tab
173
+ * above it would hold.
174
+ */
175
+ export interface SubmissionsPage extends InboxPage<SubmissionRow> {
176
+ columns: SubmissionColumn[];
177
+ counts: SubmissionCounts;
178
+ }
179
+ /** One answer, with the question as it was asked at the time (§2.2). */
180
+ export interface SubmissionValue {
181
+ id: number;
182
+ field_id: number | null;
183
+ name: string;
184
+ label: string | null;
185
+ type: string;
186
+ value: string | null;
187
+ payload: unknown;
188
+ }
189
+ export interface SubmissionAttachment {
190
+ id: number;
191
+ field_id: number | null;
192
+ name: string;
193
+ size: number;
194
+ mime: string | null;
195
+ /** Through the panel, behind the same permission as the submission (§8). */
196
+ url: string;
197
+ }
198
+ /** One line of what has happened to a submission. Never edited, never deleted. */
199
+ export interface SubmissionEvent {
200
+ id: number;
201
+ type: 'created' | 'status' | 'assignee' | 'note' | 'notified' | string;
202
+ from: string | null;
203
+ to: string | null;
204
+ /** Null is the system — the submission arriving, the notification going out. */
205
+ author: {
206
+ id: number;
207
+ name: string | null;
208
+ } | null;
209
+ created_at: string | null;
210
+ }
211
+ /** What the intake saw around the submission: the page, the language, the campaign. */
212
+ export interface SubmissionMeta {
213
+ ip?: string;
214
+ user_agent?: string;
215
+ page?: string;
216
+ referrer?: string;
217
+ utm?: Record<string, string>;
218
+ locale?: string;
219
+ [key: string]: unknown;
220
+ }
221
+ export interface InboxSubmission {
222
+ id: number;
223
+ form: {
224
+ id: number;
225
+ slug: string | null;
226
+ title: LocalizedValue | null;
227
+ };
228
+ status: InboxStatus | null;
229
+ assignee: InboxAdmin | null;
230
+ values: SubmissionValue[];
231
+ files: SubmissionAttachment[];
232
+ meta: SubmissionMeta;
233
+ events: SubmissionEvent[];
234
+ is_read: boolean;
235
+ source: string;
236
+ notified_at: string | null;
237
+ notify_error: string | null;
238
+ /** The neighbours in the list this was opened from, so the arrows walk the same pile. */
239
+ previous_id: number | null;
240
+ next_id: number | null;
241
+ created_at: string | null;
242
+ updated_at: string | null;
243
+ }
244
+ /** Everything the list asks the server for. The tab is `view`, the rest are filters. */
245
+ export interface SubmissionQuery {
246
+ view?: string;
247
+ search?: string;
248
+ /** An administrator's id, or `none` — the pile nobody has picked up. */
249
+ assignee?: string | null;
250
+ sort?: string | null;
251
+ page?: number;
252
+ per_page?: number;
253
+ }
254
+ /** A submission changed by hand: the status, the assignee, a typo in an answer. */
255
+ export interface SubmissionInput {
256
+ status_id?: number;
257
+ assignee_id?: number | null;
258
+ /** Machine name → the corrected text. The snapshot beside it is never rewritten. */
259
+ values?: Record<string, string | null>;
260
+ }
261
+ /** A pile dealt with at once. */
262
+ export interface SubmissionMassInput {
263
+ ids: number[];
264
+ action: 'status' | 'read' | 'unread' | 'delete';
265
+ status_id?: number;
266
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@webx-ui/module-inbox",
3
+ "version": "0.1.0",
4
+ "description": "Forms and submissions for the WebX UI admin panel: the forms of a site, their fields, their statuses, and what has come in through them.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": [
8
+ "*.css"
9
+ ],
10
+ "keywords": [
11
+ "webx-ui",
12
+ "vue",
13
+ "admin",
14
+ "cms",
15
+ "forms",
16
+ "inbox"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/webx-ui/webx-ui.git",
21
+ "directory": "packages/module-inbox"
22
+ },
23
+ "homepage": "https://webx-ui.github.io/webx-ui/guide/inbox.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
+ "scripts": {
43
+ "build": "vite build",
44
+ "dev": "vite build --watch",
45
+ "typecheck": "vue-tsc -p tsconfig.json --noEmit"
46
+ },
47
+ "peerDependencies": {
48
+ "vue": "^3.5.0",
49
+ "vue-router": "^4.5.0"
50
+ },
51
+ "dependencies": {
52
+ "@webx-ui/core": "^0.22.0",
53
+ "@webx-ui/module-admin": "^0.7.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^24.10.1",
57
+ "@vitejs/plugin-vue": "^6.0.1",
58
+ "typescript": "^5.9.3",
59
+ "vite": "^7.1.14",
60
+ "vite-plugin-dts": "^4.5.4",
61
+ "vue": "^3.5.24",
62
+ "vue-router": "^4.5.1",
63
+ "vue-tsc": "^3.1.3"
64
+ }
65
+ }