@navette-js/panels 1.0.0-beta.1 → 1.0.0-beta.3

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.
@@ -14,17 +14,17 @@ export type NotificationData = {
14
14
  iconColor: string | null;
15
15
  actions?: NotificationActionData[];
16
16
  };
17
- /** Ce qu'il faut d'un stockage de notifications pour y écrire (voir `DatabaseNotificationStore`). */
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
- /** Exécute `fn` dans le contexte d'une requête : `Notification.send()` y dépose ses notifications. */
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
- /** Retire et rend les notifications des lots demandés (tous si `batches` est nul). */
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
- /** Liens proposés sous le texte. Ils ne sont affichés que dans le panneau des notifications en base. */
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
- /** Enregistre la notification pour cet utilisateur (cloche du panel) : `await Notification.make()….sendToDatabase(panel.getDatabaseNotifications(), user.id)`. Sans stockage, rien n'est fait. */
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
- /** Dépose la notification pour le navigateur de la requête en cours. */
52
+ /** Queues the notification for the browser of the current request. */
53
53
  send(): this;
54
54
  }
55
- /** Port de Notification::toEmbeddedHtml (sans date, actions ni mode « inline »). Le titre et le corps sont échappés. */
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
- /** Conteneur des notifications (port de Filament\Notifications\Livewire\Notifications). */
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
- /** À l'affichage d'un document (hors navigation SPA et préchargement), les notifications en attente sont tirées tout de suite. */
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;
@@ -1,16 +1,16 @@
1
- // Notifications flash — port de filament/notifications : Notification, boîte aux lettres, composant d'affichage.
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
- /** Exécute `fn` dans le contexte d'une requête : `Notification.send()` y dépose ses notifications. */
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
- // Boîte aux lettres en mémoire, par navigateur (identifiant = jeton CSRF) puis par lot. Filament utilise le cache de l'application.
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
- /** Retire et rend les notifications des lots demandés (tous si `batches` est nul). */
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
- /** Liens proposés sous le texte. Ils ne sont affichés que dans le panneau des notifications en base. */
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
- /** Enregistre la notification pour cet utilisateur (cloche du panel) : `await Notification.make()….sendToDatabase(panel.getDatabaseNotifications(), user.id)`. Sans stockage, rien n'est fait. */
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
- /** Dépose la notification pour le navigateur de la requête en cours. */
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() doit être appelé pendant le traitement d\'une requête.');
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
- /** Port de Notification::toEmbeddedHtml (sans date, actions ni mode « inline »). Le titre et le corps sont échappés. */
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
- /** Conteneur des notifications (port de Filament\Notifications\Livewire\Notifications). */
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
- /** À l'affichage d'un document (hors navigation SPA et préchargement), les notifications en attente sont tirées tout de suite. */
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, 'Lots de notifications invalides.');
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
- /** Corps d'une notification : du HTML est nettoyé (balises de l'éditeur de texte enrichi seulement), un texte simple est échappé. */
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
- /** Suffixe du champ d'après la plateforme du navigateur, comme `globalSearchFieldKeyBindingSuffix()` : rien si elle est inconnue. */
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
- /** Stockage des notifications d'un utilisateur. `knexNotificationStore()` en fournit un sur la table `notifications` de Laravel. */
36
+ /** Storage for a user's notifications. `knexNotificationStore()` provides one backed by a `notifications` table. */
37
37
  export interface DatabaseNotificationStore {
38
- /** Notifications les plus récentes d'abord ; `offset` : nombre à sauter (pagination). */
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
- * Stockage sur la table `notifications` de Laravel, au format de Filament : les notifications déjà présentes sont lues telles quelles,
52
- * et celles envoyées d'ici restent lisibles par une application Filament. `mapUrl` réécrit l'adresse des actions lues en base.
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
- /** « 2 days ago », comme `diffForHumans()` de Carbon. */
59
+ /** Relative time in English, e.g. "2 days ago". */
60
60
  export declare function timeAgo(date: Date, now?: Date): string;
61
- /** Cloche et panneau des notifications enregistrées en base pour l'utilisateur connecté. */
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
- /** Page du panneau (pagination simple, comme `simplePaginate(50)` de Filament). */
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
- /** Nombre de notifications par page du panneau (50, comme Filament). */
76
+ /** Notifications per page. */
77
77
  static limit: number;
78
78
  isOpen: boolean;
79
- /** Le stockage et l'utilisateur viennent du serveur : chaque opération est bornée aux notifications de cet utilisateur. */
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
- // Recherche globale et notifications en base — ports de Filament\Livewire\GlobalSearch et de
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
- /** Corps d'une notification : du HTML est nettoyé (balises de l'éditeur de texte enrichi seulement), un texte simple est échappé. */
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
- /** Seules une adresse du site (`/…`) ou une adresse http(s) peuvent devenir un lien. */
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
- /** Suffixe du champ d'après la plateforme du navigateur, comme `globalSearchFieldKeyBindingSuffix()` : rien si elle est inconnue. */
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
- * Stockage sur la table `notifications` de Laravel, au format de Filament : les notifications déjà présentes sont lues telles quelles,
88
- * et celles envoyées d'ici restent lisibles par une application Filament. `mapUrl` réécrit l'adresse des actions lues en base.
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 { /* données illisibles : notification vide */ }
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
- /** « 2 days ago », comme `diffForHumans()` de Carbon. */
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
- /** Notification affichée dans le panneau (mode « inline » de Filament). Titre et corps sont échappés. */
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
- /** Cloche et panneau des notifications enregistrées en base pour l'utilisateur connecté. */
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
- /** Page du panneau (pagination simple, comme `simplePaginate(50)` de Filament). */
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
- /** Nombre de notifications par page du panneau (50, comme Filament). */
167
+ /** Notifications per page. */
169
168
  static limit = 50;
170
169
  isOpen = false;
171
- /** Le stockage et l'utilisateur viennent du serveur : chaque opération est bornée aux notifications de cet utilisateur. */
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, 'Non authentifié.');
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
- // La fenêtre porte `autofocus` : le piège de focus s'y pose, et non sur « Mark all as read » qu'Entrée déclencherait aussitôt.
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.1",
4
- "description": "Admin panels for Node.js: a port of Filament (resources, pages, navigation, authentication, tenancy).",
3
+ "version": "1.0.0-beta.3",
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.1",
48
- "@navette-js/schemas": "1.0.0-beta.1",
49
- "@navette-js/forms": "1.0.0-beta.1",
50
- "@navette-js/actions": "1.0.0-beta.1",
51
- "@navette-js/wire": "1.0.0-beta.1",
52
- "@navette-js/tables": "1.0.0-beta.1",
53
- "@navette-js/infolists": "1.0.0-beta.1",
54
- "@navette-js/database": "1.0.0-beta.1",
46
+ "@navette-js/support": "1.0.0-beta.3",
47
+ "@navette-js/schemas": "1.0.0-beta.3",
48
+ "@navette-js/forms": "1.0.0-beta.3",
49
+ "@navette-js/actions": "1.0.0-beta.3",
50
+ "@navette-js/wire": "1.0.0-beta.3",
51
+ "@navette-js/tables": "1.0.0-beta.3",
52
+ "@navette-js/infolists": "1.0.0-beta.3",
53
+ "@navette-js/database": "1.0.0-beta.3",
55
54
  "express": "^5.1.0"
56
55
  },
57
56
  "publishConfig": {
package/theme.css CHANGED
@@ -1,8 +1,8 @@
1
- /* Thème de base de Navette : le thème de FilamentPHP (MIT), compilé par Tailwind avec les sources de l'application.
2
- Une application l'importe depuis sa propre feuille de thème, puis déclare ses sources avec `@source`. */
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
- /* Classes utilitaires écrites dans les vues et le code de Navette lui-même. */
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
- <%# Logo ou nom de marque — port de filament-panels::components.logo %>
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
- <%# Menu du locataire — port de filament-panels::components.tenant-menu (sans recherche au-delà de dix locataires : voir README) %>
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
- <%# Menu utilisateur (sidebar ou barre du haut) — port de filament-panels::components.user-menu, theme-switcher et dropdown %>
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
- <%# Écart assumé avec Filament : choix d'un thème prédéfini du panel (Panel.themes()). %>
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']) %>">
@@ -1,4 +1,4 @@
1
- <%# Document HTML d'un panel — port de filament-panels::components.layout.base %>
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
- <%# Thème compilé par l'application s'il existe, sinon la feuille par défaut de Filament %>
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) { %>
@@ -1,4 +1,4 @@
1
- <%# Mise en page d'un panel — port de filament-panels::components.layout.index %>
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') %>
@@ -1,4 +1,4 @@
1
- <%# Mise en page « simple » — port de filament-panels::components.layout.simple %>
1
+ <%# Simple layout (login, registration…) %>
2
2
  <div class="fi-simple-layout">
3
3
  <% if (locals.header) { %>
4
4
  <a href="#fi-main-content" class="fi-skip-link fi-sr-only">
@@ -1,4 +1,4 @@
1
- <%# Sidebar — port de filament-panels::livewire.sidebar, components.sidebar.group et components.sidebar.item %>
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
- <%# Comme Filament : après le pied de sidebar, pas dedans. %>
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
- <%# Barre du haut — port de filament-panels::livewire.topbar et components.topbar.item %>
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) { %>
@@ -1,11 +1,11 @@
1
- <%# Page d'un panel — port de filament-panels::components.page.index, components.header.index et filament::components.breadcrumbs %>
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
- <%# Sous-navigation : menu déroulant sur mobile ; sur grand écran, onglets (top) ou colonne (start, end) %>
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
- <%# Comme Filament : le conteneur existe dès qu'il y a une action ou qu'un des deux emplacements rend quelque chose. %>
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 %>
@@ -1,4 +1,4 @@
1
- <%# Page « simple » — port de filament-panels::components.page.simple + header.simple + logo %>
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">