@navette-js/panels 1.0.0-beta.1 → 1.0.0-beta.2
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 +2 -2
- package/THIRD-PARTY-NOTICES.md +2 -2
- package/lib/auth.d.ts +16 -16
- package/lib/auth.js +19 -21
- package/lib/calendar.d.ts +18 -18
- package/lib/calendar.js +10 -10
- package/lib/index.d.ts +333 -328
- package/lib/index.js +509 -464
- package/lib/notifications.d.ts +9 -9
- package/lib/notifications.js +12 -12
- package/lib/topbar.d.ts +11 -11
- package/lib/topbar.js +15 -16
- package/package.json +10 -11
- package/theme.css +3 -3
- package/views/components/logo.ejs +1 -1
- package/views/components/tenant-menu.ejs +1 -1
- package/views/components/user-menu.ejs +2 -2
- package/views/layout/base.ejs +2 -2
- package/views/layout/index.ejs +1 -1
- package/views/layout/simple.ejs +1 -1
- package/views/livewire/sidebar.ejs +2 -2
- package/views/livewire/topbar.ejs +1 -1
- package/views/pages/page.ejs +3 -3
- package/views/pages/simple.ejs +1 -1
package/lib/notifications.d.ts
CHANGED
|
@@ -14,17 +14,17 @@ export type NotificationData = {
|
|
|
14
14
|
iconColor: string | null;
|
|
15
15
|
actions?: NotificationActionData[];
|
|
16
16
|
};
|
|
17
|
-
/**
|
|
17
|
+
/** The part of a notification store needed to write to it (see `DatabaseNotificationStore`). */
|
|
18
18
|
type NotificationSink = {
|
|
19
19
|
send(user: string | number, notification: NotificationData): Promise<void>;
|
|
20
20
|
};
|
|
21
|
-
/**
|
|
21
|
+
/** Runs `fn` in a request context, where `Notification.send()` queues its notifications. */
|
|
22
22
|
export declare function runInRequest<T>(mailbox: string, fn: () => Promise<T>): Promise<{
|
|
23
23
|
result: T;
|
|
24
24
|
pending: NotificationData[];
|
|
25
25
|
}>;
|
|
26
26
|
export declare function publish(mailbox: string, notifications: NotificationData[]): string;
|
|
27
|
-
/**
|
|
27
|
+
/** Removes and returns the notifications of the given batches (all of them if `batches` is null). */
|
|
28
28
|
export declare function pull(mailbox: string, batches: string[] | null): NotificationData[];
|
|
29
29
|
export declare class Notification {
|
|
30
30
|
private data;
|
|
@@ -40,21 +40,21 @@ export declare class Notification {
|
|
|
40
40
|
iconColor(color: string): this;
|
|
41
41
|
duration(ms: number): this;
|
|
42
42
|
persistent(): this;
|
|
43
|
-
/**
|
|
43
|
+
/** Links shown under the text. Only displayed in the database notifications panel. */
|
|
44
44
|
actions(actions: {
|
|
45
45
|
label: string;
|
|
46
46
|
url?: string | null;
|
|
47
47
|
openUrlInNewTab?: boolean;
|
|
48
48
|
}[]): this;
|
|
49
49
|
getData(): NotificationData;
|
|
50
|
-
/**
|
|
50
|
+
/** Stores the notification for this user (panel bell): `await Notification.make()….sendToDatabase(panel.getDatabaseNotifications(), user.id)`. Does nothing without a store. */
|
|
51
51
|
sendToDatabase(store: NotificationSink | null, user: string | number): Promise<this>;
|
|
52
|
-
/**
|
|
52
|
+
/** Queues the notification for the browser of the current request. */
|
|
53
53
|
send(): this;
|
|
54
54
|
}
|
|
55
|
-
/**
|
|
55
|
+
/** Renders a flash notification (no date, actions or inline mode). Title and body are escaped. */
|
|
56
56
|
export declare function renderNotification(n: NotificationData): string;
|
|
57
|
-
/**
|
|
57
|
+
/** Container component for flash notifications. */
|
|
58
58
|
export declare class Notifications extends Component {
|
|
59
59
|
static callable: string[];
|
|
60
60
|
isFilamentNotificationsComponent: boolean;
|
|
@@ -66,7 +66,7 @@ export declare class Notifications extends Component {
|
|
|
66
66
|
notificationClosed: string;
|
|
67
67
|
};
|
|
68
68
|
private get mailbox();
|
|
69
|
-
/**
|
|
69
|
+
/** On a full document load (not SPA navigation or prefetch), pending notifications are pulled right away. */
|
|
70
70
|
mount(): void;
|
|
71
71
|
pullNotifications(batches?: unknown): void;
|
|
72
72
|
pullNotificationsFromSession(batches?: unknown): void;
|
package/lib/notifications.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Flash notifications: the Notification builder, a per-browser mailbox, and the component that displays them.
|
|
2
2
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
3
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
4
4
|
import { Attributes, e, icon, js } from '@navette-js/support';
|
|
5
5
|
import { t } from '@navette-js/support/i18n';
|
|
6
6
|
import { Component, WireError } from '@navette-js/wire';
|
|
7
7
|
const requestStore = new AsyncLocalStorage();
|
|
8
|
-
/**
|
|
8
|
+
/** Runs `fn` in a request context, where `Notification.send()` queues its notifications. */
|
|
9
9
|
export function runInRequest(mailbox, fn) {
|
|
10
10
|
const store = { mailbox, pending: [] };
|
|
11
11
|
return requestStore.run(store, async () => ({ result: await fn(), pending: store.pending }));
|
|
12
12
|
}
|
|
13
|
-
//
|
|
13
|
+
// In-memory mailbox, keyed by browser (the CSRF token) then by batch.
|
|
14
14
|
const mailboxes = new Map();
|
|
15
15
|
const TTL = 5 * 60 * 1000;
|
|
16
16
|
export function publish(mailbox, notifications) {
|
|
@@ -23,7 +23,7 @@ export function publish(mailbox, notifications) {
|
|
|
23
23
|
mailboxes.set(mailbox, box);
|
|
24
24
|
return batch;
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
26
|
+
/** Removes and returns the notifications of the given batches (all of them if `batches` is null). */
|
|
27
27
|
export function pull(mailbox, batches) {
|
|
28
28
|
const box = mailboxes.get(mailbox);
|
|
29
29
|
if (!box)
|
|
@@ -53,22 +53,22 @@ export class Notification {
|
|
|
53
53
|
iconColor(color) { this.data.iconColor = color; return this; }
|
|
54
54
|
duration(ms) { this.data.duration = ms; return this; }
|
|
55
55
|
persistent() { this.data.duration = 'persistent'; return this; }
|
|
56
|
-
/**
|
|
56
|
+
/** Links shown under the text. Only displayed in the database notifications panel. */
|
|
57
57
|
actions(actions) { this.data.actions = actions.slice(0, 10).map((action) => ({ label: String(action.label), url: action.url ?? null, openUrlInNewTab: action.openUrlInNewTab === true })); return this; }
|
|
58
58
|
getData() { return { ...this.data }; }
|
|
59
|
-
/**
|
|
59
|
+
/** Stores the notification for this user (panel bell): `await Notification.make()….sendToDatabase(panel.getDatabaseNotifications(), user.id)`. Does nothing without a store. */
|
|
60
60
|
async sendToDatabase(store, user) { if (store)
|
|
61
61
|
await store.send(user, { ...this.data, id: randomUUID() }); return this; }
|
|
62
|
-
/**
|
|
62
|
+
/** Queues the notification for the browser of the current request. */
|
|
63
63
|
send() {
|
|
64
64
|
const store = requestStore.getStore();
|
|
65
65
|
if (!store)
|
|
66
|
-
throw new Error('Notification.send()
|
|
66
|
+
throw new Error('Notification.send() must be called while handling a request.');
|
|
67
67
|
store.pending.push({ ...this.data });
|
|
68
68
|
return this;
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
/**
|
|
71
|
+
/** Renders a flash notification (no date, actions or inline mode). Title and body are escaped. */
|
|
72
72
|
export function renderNotification(n) {
|
|
73
73
|
const closeLabel = t('filament-notifications::notification.actions.close.label');
|
|
74
74
|
const attributes = new Attributes()
|
|
@@ -79,7 +79,7 @@ export function renderNotification(n) {
|
|
|
79
79
|
const iconHtml = iconName ? icon(iconName, { size: 'lg', attributes: new Attributes().class([...(iconColor && iconColor !== 'gray' ? ['fi-color', `fi-color-${iconColor}`] : []), 'fi-no-notification-icon']) }) : '';
|
|
80
80
|
return `<div x-data="notificationComponent({ notification: ${e(js({ id: n.id, duration: n.duration }))} })" x-transition:enter-start="fi-transition-enter-start" x-transition:enter-end="fi-transition-enter-end" x-transition:leave-start="fi-transition-leave-start" x-transition:leave-end="fi-transition-leave-end" x-on:mouseenter="pauseDuration('hover')" x-on:mouseleave="resumeDuration('hover')" x-on:focusin="handleFocusIn($event)" x-on:focusout="handleFocusOut($event)" x-on:keydown.escape="handleEscape($event)" ${attributes.toHtml().replace(/"if \(\$event\.detail\.id == '([^']*)'\) close\(\)"/, (_m, id) => `"if ($event.detail.id == '${id}') close()"`)}>${iconHtml}<div class="fi-no-notification-main">${n.title !== null || n.body !== null ? `<div class="fi-no-notification-text">${n.title !== null ? `<h3 class="fi-no-notification-title">${e(n.title)}</h3>` : ''}${n.body !== null ? `<div class="fi-no-notification-body">${e(n.body)}</div>` : ''}</div>` : ''}</div><button type="button" x-on:click="dismiss()" aria-label="${e(closeLabel)}" title="${e(closeLabel)}" class="fi-icon-btn fi-no-notification-close-btn">${icon('m-x-mark')}</button></div>`;
|
|
81
81
|
}
|
|
82
|
-
/**
|
|
82
|
+
/** Container component for flash notifications. */
|
|
83
83
|
export class Notifications extends Component {
|
|
84
84
|
static callable = ['pullNotifications'];
|
|
85
85
|
isFilamentNotificationsComponent = true;
|
|
@@ -88,7 +88,7 @@ export class Notifications extends Component {
|
|
|
88
88
|
static locked = ['isFilamentNotificationsComponent', 'notifications', 'hasPulledNotificationsOnMount'];
|
|
89
89
|
static listeners = { notificationsSent: 'pullNotifications', notificationClosed: 'removeNotification' };
|
|
90
90
|
get mailbox() { return String(this._ctx?.mailbox ?? ''); }
|
|
91
|
-
/**
|
|
91
|
+
/** On a full document load (not SPA navigation or prefetch), pending notifications are pulled right away. */
|
|
92
92
|
mount() {
|
|
93
93
|
const req = this._ctx?.req;
|
|
94
94
|
if (!req || req.method !== 'GET' || req.headers['sec-fetch-dest'] !== 'document' || req.headers['x-livewire-navigate'] !== undefined || String(req.headers['sec-purpose'] ?? '').toLowerCase().includes('prefetch'))
|
|
@@ -99,7 +99,7 @@ export class Notifications extends Component {
|
|
|
99
99
|
}
|
|
100
100
|
pullNotifications(batches = []) {
|
|
101
101
|
if (!Array.isArray(batches) || batches.length > 50 || batches.some((b) => typeof b !== 'string' || !/^[a-f0-9]{40}$/.test(b)))
|
|
102
|
-
throw new WireError(422, '
|
|
102
|
+
throw new WireError(422, 'Invalid notification batches.');
|
|
103
103
|
for (const n of pull(this.mailbox, batches))
|
|
104
104
|
this.notifications[n.id] = n;
|
|
105
105
|
}
|
package/lib/topbar.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Component } from '@navette-js/wire';
|
|
2
2
|
import type { NotificationData } from './notifications.ts';
|
|
3
|
-
/**
|
|
3
|
+
/** Notification body: HTML is sanitized (rich editor tags only), plain text is escaped. */
|
|
4
4
|
export declare const notificationBodyHtml: (body: string) => string;
|
|
5
5
|
export type GlobalSearchResult = {
|
|
6
6
|
title: string;
|
|
@@ -11,7 +11,7 @@ export type GlobalSearchCategory = {
|
|
|
11
11
|
label: string;
|
|
12
12
|
results: GlobalSearchResult[];
|
|
13
13
|
};
|
|
14
|
-
/**
|
|
14
|
+
/** Search field suffix showing the first key binding for the browser's platform; `null` if the platform is unknown. */
|
|
15
15
|
export declare function keyBindingSuffix(keyBindings: string[], userAgent: string): string | null;
|
|
16
16
|
export declare class GlobalSearch extends Component {
|
|
17
17
|
search: string;
|
|
@@ -33,9 +33,9 @@ export type DatabaseNotificationRecord = {
|
|
|
33
33
|
createdAt: Date | null;
|
|
34
34
|
unread: boolean;
|
|
35
35
|
};
|
|
36
|
-
/**
|
|
36
|
+
/** Storage for a user's notifications. `knexNotificationStore()` provides one backed by a `notifications` table. */
|
|
37
37
|
export interface DatabaseNotificationStore {
|
|
38
|
-
/**
|
|
38
|
+
/** Most recent first; `offset` is the number of notifications to skip (pagination). */
|
|
39
39
|
list(user: string | number, limit: number, offset?: number): Promise<DatabaseNotificationRecord[]>;
|
|
40
40
|
count(user: string | number): Promise<number>;
|
|
41
41
|
unreadCount(user: string | number): Promise<number>;
|
|
@@ -48,20 +48,20 @@ export interface DatabaseNotificationStore {
|
|
|
48
48
|
clear(user: string | number): Promise<void>;
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
51
|
+
* Store backed by a `notifications` table (polymorphic `notifiable_type`/`notifiable_id`, JSON `data` with `format: 'filament'`).
|
|
52
|
+
* Existing rows in that format are read as-is. `mapUrl` rewrites action URLs read from the table.
|
|
53
53
|
*/
|
|
54
54
|
export declare function knexNotificationStore(knex: any, options?: {
|
|
55
55
|
table?: string;
|
|
56
56
|
notifiableType?: string;
|
|
57
57
|
mapUrl?: (url: string) => string;
|
|
58
58
|
}): DatabaseNotificationStore;
|
|
59
|
-
/**
|
|
59
|
+
/** Relative time in English, e.g. "2 days ago". */
|
|
60
60
|
export declare function timeAgo(date: Date, now?: Date): string;
|
|
61
|
-
/**
|
|
61
|
+
/** Bell and slide-over listing the signed-in user's database notifications. */
|
|
62
62
|
export declare class DatabaseNotifications extends Component {
|
|
63
63
|
static callable: string[];
|
|
64
|
-
/**
|
|
64
|
+
/** Current page of the panel (simple pagination). */
|
|
65
65
|
paginators: Record<string, number>;
|
|
66
66
|
private get notificationsPage();
|
|
67
67
|
previousPage(): void;
|
|
@@ -73,10 +73,10 @@ export declare class DatabaseNotifications extends Component {
|
|
|
73
73
|
markedNotificationAsRead: string;
|
|
74
74
|
markedNotificationAsUnread: string;
|
|
75
75
|
};
|
|
76
|
-
/**
|
|
76
|
+
/** Notifications per page. */
|
|
77
77
|
static limit: number;
|
|
78
78
|
isOpen: boolean;
|
|
79
|
-
/**
|
|
79
|
+
/** Store and user come from the server: every operation is restricted to this user's notifications. */
|
|
80
80
|
private get scope();
|
|
81
81
|
refresh(): void;
|
|
82
82
|
removeNotification(id: unknown): Promise<void>;
|
package/lib/topbar.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Filament\Notifications\Livewire\DatabaseNotifications. Les deux composants vivent dans la sidebar du panel.
|
|
1
|
+
// Global search and database notifications: two components that live in the panel sidebar.
|
|
3
2
|
import { Attributes, e, icon, js, loadingIndicator, renderHook } from '@navette-js/support';
|
|
4
3
|
import { t } from '@navette-js/support/i18n';
|
|
5
4
|
import { Component, WireError } from '@navette-js/wire';
|
|
6
5
|
import { richDocumentToHtml, richHtmlToDocument } from '@navette-js/forms';
|
|
7
6
|
import { pagination } from '@navette-js/support/ui';
|
|
8
|
-
/**
|
|
7
|
+
/** Notification body: HTML is sanitized (rich editor tags only), plain text is escaped. */
|
|
9
8
|
export const notificationBodyHtml = (body) => (/<[a-z][\s\S]*>/i.test(body) ? (richDocumentToHtml(richHtmlToDocument(body)) ?? '') : e(body));
|
|
10
9
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
10
|
const safeIcon = (name, size, classes) => {
|
|
@@ -18,9 +17,9 @@ const safeIcon = (name, size, classes) => {
|
|
|
18
17
|
return '';
|
|
19
18
|
}
|
|
20
19
|
};
|
|
21
|
-
/**
|
|
20
|
+
/** Only a same-site path (`/…`) or an http(s) URL can become a link (no `javascript:` or protocol-relative URLs). */
|
|
22
21
|
const safeUrl = (url) => (typeof url === 'string' && /^(\/(?!\/)|https?:\/\/)/.test(url) ? url : null);
|
|
23
|
-
/**
|
|
22
|
+
/** Search field suffix showing the first key binding for the browser's platform; `null` if the platform is unknown. */
|
|
24
23
|
export function keyBindingSuffix(keyBindings, userAgent) {
|
|
25
24
|
const first = keyBindings[0];
|
|
26
25
|
if (!first)
|
|
@@ -84,8 +83,8 @@ export class GlobalSearch extends Component {
|
|
|
84
83
|
}
|
|
85
84
|
const stamp = (date) => date.toISOString().slice(0, 19).replace('T', ' ');
|
|
86
85
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
86
|
+
* Store backed by a `notifications` table (polymorphic `notifiable_type`/`notifiable_id`, JSON `data` with `format: 'filament'`).
|
|
87
|
+
* Existing rows in that format are read as-is. `mapUrl` rewrites action URLs read from the table.
|
|
89
88
|
*/
|
|
90
89
|
export function knexNotificationStore(knex, options = {}) {
|
|
91
90
|
const table = options.table ?? 'notifications';
|
|
@@ -100,7 +99,7 @@ export function knexNotificationStore(knex, options = {}) {
|
|
|
100
99
|
try {
|
|
101
100
|
data = JSON.parse(row.data) ?? {};
|
|
102
101
|
}
|
|
103
|
-
catch { /*
|
|
102
|
+
catch { /* unreadable data: empty notification */ }
|
|
104
103
|
const iconName = text(data.icon)?.replace(/^heroicon-/, '') ?? null;
|
|
105
104
|
return {
|
|
106
105
|
id: String(row.id), title: text(data.title), body: text(data.body), icon: iconName, iconColor: text(data.iconColor), status: text(data.status),
|
|
@@ -125,7 +124,7 @@ export function knexNotificationStore(knex, options = {}) {
|
|
|
125
124
|
async clear(user) { await scope(user).delete(); },
|
|
126
125
|
};
|
|
127
126
|
}
|
|
128
|
-
/**
|
|
127
|
+
/** Relative time in English, e.g. "2 days ago". */
|
|
129
128
|
export function timeAgo(date, now = new Date()) {
|
|
130
129
|
const seconds = Math.max(0, Math.floor((now.getTime() - date.getTime()) / 1000));
|
|
131
130
|
const units = [[365 * 86400, 'year'], [30 * 86400, 'month'], [7 * 86400, 'week'], [86400, 'day'], [3600, 'hour'], [60, 'minute'], [1, 'second']];
|
|
@@ -137,7 +136,7 @@ export function timeAgo(date, now = new Date()) {
|
|
|
137
136
|
}
|
|
138
137
|
return '0 seconds ago';
|
|
139
138
|
}
|
|
140
|
-
/**
|
|
139
|
+
/** A notification rendered inline in the panel. Title and body are escaped. */
|
|
141
140
|
function renderDatabaseNotification(n) {
|
|
142
141
|
const color = n.iconColor ?? n.status;
|
|
143
142
|
const iconHtml = safeIcon(n.icon ?? (n.status ? { danger: 'o-x-circle', info: 'o-information-circle', success: 'o-check-circle', warning: 'o-exclamation-circle' }[n.status] ?? null : null), 'md', [...(color && color !== 'gray' && /^[a-z]+$/.test(color) ? ['fi-color', `fi-color-${color}`] : []), 'fi-no-notification-icon']);
|
|
@@ -155,25 +154,25 @@ function renderDatabaseNotification(n) {
|
|
|
155
154
|
}
|
|
156
155
|
const BADGE = 'fi-color fi-color-primary fi-text-color-600 dark:fi-text-color-200 fi-badge';
|
|
157
156
|
const linkAction = (label, method, color, extra = '') => `<button type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled" class="fi-ac-link-action fi-link fi-size-md fi-color fi-color-${color} fi-text-color-600 dark:fi-text-color-300" wire:click="${method}"${extra}>${loadingIndicator(new Attributes({ 'wire:loading.delay.default': '', 'wire:target': method }))}<span class="fi-link-label">${e(label)}</span></button>`;
|
|
158
|
-
/**
|
|
157
|
+
/** Bell and slide-over listing the signed-in user's database notifications. */
|
|
159
158
|
export class DatabaseNotifications extends Component {
|
|
160
159
|
static callable = ['refresh', 'removeNotification', 'markNotificationAsRead', 'markNotificationAsUnread', 'clearNotifications', 'markAllNotificationsAsRead', 'previousPage', 'nextPage', 'gotoPage'];
|
|
161
|
-
/**
|
|
160
|
+
/** Current page of the panel (simple pagination). */
|
|
162
161
|
paginators = { 'database-notifications-page': 1 };
|
|
163
162
|
get notificationsPage() { return Math.max(1, Math.min(10_000, Math.floor(Number(this.paginators?.['database-notifications-page']) || 1))); }
|
|
164
163
|
previousPage() { this.paginators = { 'database-notifications-page': Math.max(1, this.notificationsPage - 1) }; }
|
|
165
164
|
nextPage() { this.paginators = { 'database-notifications-page': this.notificationsPage + 1 }; }
|
|
166
165
|
gotoPage(page) { this.paginators = { 'database-notifications-page': Math.max(1, Math.floor(Number(page)) || 1) }; }
|
|
167
166
|
static listeners = { databaseNotificationsSent: 'refresh', notificationClosed: 'removeNotification', markedNotificationAsRead: 'markNotificationAsRead', markedNotificationAsUnread: 'markNotificationAsUnread' };
|
|
168
|
-
/**
|
|
167
|
+
/** Notifications per page. */
|
|
169
168
|
static limit = 50;
|
|
170
169
|
isOpen = false;
|
|
171
|
-
/**
|
|
170
|
+
/** Store and user come from the server: every operation is restricted to this user's notifications. */
|
|
172
171
|
get scope() {
|
|
173
172
|
const { panel, user } = (this._ctx ?? {});
|
|
174
173
|
const store = panel?.getDatabaseNotifications() ?? null;
|
|
175
174
|
if (!user || !store)
|
|
176
|
-
throw new WireError(401, '
|
|
175
|
+
throw new WireError(401, 'Unauthenticated.');
|
|
177
176
|
return { store, user: user.id, interval: panel.getDatabaseNotificationsPollingInterval() };
|
|
178
177
|
}
|
|
179
178
|
refresh() { }
|
|
@@ -232,7 +231,7 @@ export class DatabaseNotifications extends Component {
|
|
|
232
231
|
const poll = interval && /^[0-9]+(ms|s)$/.test(interval) ? ` wire:poll.${interval}=""` : '';
|
|
233
232
|
const windowClasses = ['fi-modal-window', 'fi-modal-window-has-close-btn', has ? 'fi-modal-window-has-content' : 'fi-modal-window-has-icon', `fi-align-${has ? 'start' : 'center'}`, 'fi-width-md'].join(' ');
|
|
234
233
|
const transitions = 'x-transition:enter="fi-transition-enter" x-transition:leave="fi-transition-leave" x-transition:enter-start="fi-transition-enter-start" x-transition:enter-end="fi-transition-enter-end" x-transition:leave-start="fi-transition-leave-start" x-transition:leave-end="fi-transition-leave-end"';
|
|
235
|
-
//
|
|
234
|
+
// The window has `autofocus` so the focus trap lands on it, not on "Mark all as read", which Enter would trigger at once.
|
|
236
235
|
const dialog = `<div ${modal.toHtml()}${poll}><div aria-hidden="true" x-show="isOpen" x-transition.duration.300ms.opacity class="fi-modal-close-overlay"></div><div tabindex="-1" x-on:click.self="${e(close)}" x-trap.noreturn="isTrapActive" class="fi-modal-window-ctn fi-clickable"><div x-on:keydown.window.escape="if (isTopmost()) ${e(close)}" x-show="isWindowVisible" ${transitions} wire:key="${e(key('window'))}" tabindex="-1" autofocus="autofocus" class="${windowClasses}">${header}${content}</div></div></div>`;
|
|
237
236
|
const opened = "if ($event.detail.id === 'database-notifications' && ! $wire.isOpen) $wire.set('isOpen', true)";
|
|
238
237
|
const closed = "if ($event.detail.id === 'database-notifications') $wire.set('isOpen', false, false)";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navette-js/panels",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
4
|
-
"description": "Admin panels for Node.js:
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
|
+
"description": "Admin panels for Node.js: resources, pages, navigation, authentication, tenancy.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -36,7 +36,6 @@
|
|
|
36
36
|
},
|
|
37
37
|
"keywords": [
|
|
38
38
|
"navette",
|
|
39
|
-
"filament",
|
|
40
39
|
"admin",
|
|
41
40
|
"panel",
|
|
42
41
|
"livewire",
|
|
@@ -44,14 +43,14 @@
|
|
|
44
43
|
"node"
|
|
45
44
|
],
|
|
46
45
|
"dependencies": {
|
|
47
|
-
"@navette-js/support": "1.0.0-beta.
|
|
48
|
-
"@navette-js/schemas": "1.0.0-beta.
|
|
49
|
-
"@navette-js/forms": "1.0.0-beta.
|
|
50
|
-
"@navette-js/actions": "1.0.0-beta.
|
|
51
|
-
"@navette-js/wire": "1.0.0-beta.
|
|
52
|
-
"@navette-js/tables": "1.0.0-beta.
|
|
53
|
-
"@navette-js/infolists": "1.0.0-beta.
|
|
54
|
-
"@navette-js/database": "1.0.0-beta.
|
|
46
|
+
"@navette-js/support": "1.0.0-beta.2",
|
|
47
|
+
"@navette-js/schemas": "1.0.0-beta.2",
|
|
48
|
+
"@navette-js/forms": "1.0.0-beta.2",
|
|
49
|
+
"@navette-js/actions": "1.0.0-beta.2",
|
|
50
|
+
"@navette-js/wire": "1.0.0-beta.2",
|
|
51
|
+
"@navette-js/tables": "1.0.0-beta.2",
|
|
52
|
+
"@navette-js/infolists": "1.0.0-beta.2",
|
|
53
|
+
"@navette-js/database": "1.0.0-beta.2",
|
|
55
54
|
"express": "^5.1.0"
|
|
56
55
|
},
|
|
57
56
|
"publishConfig": {
|
package/theme.css
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
1
|
+
/* Navette's base theme (MIT-licensed sources under resources/), compiled by Tailwind with the application's sources.
|
|
2
|
+
An application imports it from its own theme stylesheet, then declares its sources with `@source`. */
|
|
3
3
|
@import './resources/filament/packages/panels/resources/css/theme.css';
|
|
4
4
|
|
|
5
|
-
/*
|
|
5
|
+
/* Utility classes used in Navette's own views and code. */
|
|
6
6
|
@source './views/**/*.ejs';
|
|
7
7
|
@source './src/**/*.ts';
|
|
8
8
|
@source '../forms/src/**/*.ts';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# Brand logo or brand name %>
|
|
2
2
|
<% if (brand.logo) { %>
|
|
3
3
|
<img alt="<%= t('filament-panels::layout.logo.alt', { name: brand.name }) %>" src="<%= brand.logo %>" style="height: <%- brand.height %>;" class="<%- brand.darkLogo ? 'fi-logo fi-logo-light' : 'fi-logo' %>" />
|
|
4
4
|
<% if (brand.darkLogo) { %>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# Tenant menu (no search, even beyond ten tenants) %>
|
|
2
2
|
<%- renderHook('panels::tenant-menu.before') %>
|
|
3
3
|
<div x-data="filamentDropdown({ isMenu: true })" class="fi-dropdown fi-tenant-menu" ><div x-on:mousedown="if ($event.button === 0) toggle($event)" class="fi-dropdown-trigger" ><button<% if (isSidebarCollapsibleOnDesktop) { %> x-data="{ tooltip: false }" x-effect="
|
|
4
4
|
tooltip = $store.sidebar.isOpen
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# User menu (sidebar or topbar), with the theme switcher %>
|
|
2
2
|
<% const topbar = locals.position === 'topbar'; const collapsible = locals.isSidebarCollapsibleOnDesktop === true; const before = locals.itemsBefore ?? []; const after = locals.itemsAfter ?? []; %>
|
|
3
3
|
<%- renderHook('panels::user-menu.before') %>
|
|
4
4
|
<div x-data="filamentDropdown({ isMenu: true })" class="fi-dropdown fi-user-menu">
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
</div>
|
|
52
52
|
|
|
53
53
|
<% if (locals.themes && themes.length > 1) { %>
|
|
54
|
-
<%#
|
|
54
|
+
<%# Choice of one of the panel's predefined themes (Panel.themes()). %>
|
|
55
55
|
<div role="group" aria-label="Theme" class="fi-dropdown-list nv-theme-list">
|
|
56
56
|
<% for (const theme of themes) { %>
|
|
57
57
|
<button role="menuitemradio" aria-checked="<%= theme.id === activeTheme ? 'true' : 'false' %>" tabindex="-1" type="button" wire:click="setTheme(<%= js(theme.id) %>)" data-theme="<%= theme.id %>" class="<%- cls(['fi-dropdown-list-item', theme.id === activeTheme && 'fi-active']) %>">
|
package/views/layout/base.ejs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# Panel HTML document %>
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html lang="en" dir="ltr" class="fi">
|
|
4
4
|
<head>
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
<%- colorVariables %>
|
|
46
46
|
|
|
47
|
-
<%#
|
|
47
|
+
<%# The application's compiled theme if any, otherwise the default stylesheet %>
|
|
48
48
|
<link rel="stylesheet" href="<%- locals.theme ? theme : assetUrl('css/filament/filament/app.css') %>" data-navigate-track="reload" />
|
|
49
49
|
|
|
50
50
|
<% if (font.url) { %>
|
package/views/layout/index.ejs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# Full panel layout %>
|
|
2
2
|
<% const collapsible = isSidebarCollapsibleOnDesktop || isSidebarFullyCollapsibleOnDesktop; %>
|
|
3
3
|
<a x-data="{}" x-bind:inert="$store.sidebar.isOpenMobile" href="#fi-main-content" class="fi-skip-link fi-sr-only">
|
|
4
4
|
<%= t('filament-panels::layout.skip_to_content.label') %>
|
package/views/layout/simple.ejs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%# Sidebar
|
|
1
|
+
<%# Sidebar: navigation groups and items %>
|
|
2
2
|
<% const collapsible = isSidebarCollapsibleOnDesktop || isSidebarFullyCollapsibleOnDesktop; %>
|
|
3
3
|
<div>
|
|
4
4
|
<div x-data="filamentSidebar({
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
<%- userMenu ?? '' %>
|
|
114
114
|
</div>
|
|
115
115
|
<% } %>
|
|
116
|
-
<%#
|
|
116
|
+
<%# Rendered after the sidebar footer, not inside it. %>
|
|
117
117
|
<%- renderHook('panels::sidebar.footer') %>
|
|
118
118
|
</div>
|
|
119
119
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# Topbar and its navigation items %>
|
|
2
2
|
<div x-data="filamentSidebarBackground" x-bind:inert="$store.sidebar.isOpenMobile && ! hasOpenModal" class="fi-topbar-ctn" ><nav aria-label="<%= t('filament-panels::layout.topbar.label') %>" class="fi-topbar" >
|
|
3
3
|
<%- renderHook('panels::topbar.start') %>
|
|
4
4
|
<% if (hasNavigation) { %>
|
package/views/pages/page.ejs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# Panel page: header, breadcrumbs, sub-navigation and content %>
|
|
2
2
|
<% const beforeHeaderActions = renderHook('panels::page.header.actions.before', renderHookScopes), afterHeaderActions = renderHook('panels::page.header.actions.after', renderHookScopes); %>
|
|
3
3
|
<% const subNavigation = locals.subNavigation ?? []; const activeSubItem = subNavigation.find((item) => item.active) ?? subNavigation[0]; const subNavigationPosition = locals.subNavigationPosition ?? 'top'; %>
|
|
4
4
|
<div class="<%- cls(['fi-page', subNavigation.length ? `fi-page-has-sub-navigation fi-page-has-sub-navigation-${subNavigationPosition}` : null, ...(locals.pageClasses ?? [])]) %>">
|
|
5
5
|
<%- renderHook('panels::page.start', renderHookScopes) %>
|
|
6
6
|
<div class="fi-page-header-main-ctn">
|
|
7
7
|
<% if (subNavigation.length) { %>
|
|
8
|
-
<%#
|
|
8
|
+
<%# Sub-navigation: a dropdown on mobile; on desktop, tabs (top) or a column (start, end) %>
|
|
9
9
|
<div class="fi-page-main-sub-navigation-mobile-menu-render-hook-ctn"><%- renderHook('panels::page.sub-navigation.module-menu.before', renderHookScopes) %></div>
|
|
10
10
|
<div x-data="filamentDropdown({ isMenu: true })" class="fi-dropdown fi-page-sub-navigation-dropdown">
|
|
11
11
|
<div x-on:mousedown="if ($event.button === 0) toggle($event)" class="fi-dropdown-trigger">
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
<%- renderHook('panels::page.header.heading.after', renderHookScopes) %>
|
|
55
55
|
</div>
|
|
56
56
|
|
|
57
|
-
<%#
|
|
57
|
+
<%# The container exists as soon as there is an action or either render hook outputs something. %>
|
|
58
58
|
<% if (headerActions.length || (beforeHeaderActions + afterHeaderActions).trim() !== '') { %>
|
|
59
59
|
<div class="fi-header-actions-ctn">
|
|
60
60
|
<%- beforeHeaderActions %>
|
package/views/pages/simple.ejs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%#
|
|
1
|
+
<%# Simple page: logo, header and content %>
|
|
2
2
|
<div class="fi-simple-page">
|
|
3
3
|
<%- renderHook('panels::simple-page.start', renderHookScopes) %>
|
|
4
4
|
<div class="fi-simple-page-content">
|