@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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # @navette-js/panels
2
2
 
3
- Admin panels for Node.js: a port of Filament (resources, pages, navigation, authentication, tenancy).
3
+ Admin panels for Node.js: resources, pages, navigation, authentication, tenancy.
4
4
 
5
- Part of [Navette](https://github.com/NavetteJS/navette), a Node.js port of [Filament](https://filamentphp.com). Install the whole framework with `npm install @navette-js/panels`.
5
+ Part of [Navette](https://github.com/NavetteJS/navette), admin panels for Node.js. Start a project with `npm create navette@latest`, or install the framework with `npm install @navette-js/panels`.
6
6
 
7
7
  **Beta:** the API may still change between beta releases.
8
8
 
@@ -1,10 +1,10 @@
1
1
  # Third-party notices
2
2
 
3
- Navette is an independent Node.js port of Filament. It is not affiliated with or endorsed by the Filament, Livewire or Heroicons projects. It reuses the following works, each under its own license; the full license texts ship next to the files they cover.
3
+ Navette is an independent project, not affiliated with or endorsed by the Filament, Livewire or Heroicons projects. It reuses the following works, each under its own license; the full license texts ship next to the files they cover.
4
4
 
5
5
  | Work | Used for | License | License file |
6
6
  |---|---|---|---|
7
- | [Filament](https://github.com/filamentphp/filament) © Filament | HTML markup ported from its Blade views, compiled CSS and JavaScript (`@navette-js/panels/dist/filament`), English translations (`@navette-js/support/data`) | MIT | `packages/panels/dist/FILAMENT-LICENSE.md` |
7
+ | [Filament](https://github.com/filamentphp/filament) © Filament | HTML markup derived from its Blade views, compiled CSS and JavaScript (`@navette-js/panels/dist/filament`), English translations (`@navette-js/support/data`) | MIT | `packages/panels/dist/FILAMENT-LICENSE.md` |
8
8
  | [Livewire](https://github.com/livewire/livewire) © Caleb Porzio | Client script `livewire.js` (`@navette-js/wire/vendor`); the server runtime follows its protocol | MIT | `packages/wire/vendor/LIVEWIRE-LICENSE.md` |
9
9
  | [Heroicons](https://github.com/tailwindlabs/heroicons) | SVG icons (`@navette-js/support/icons`) | MIT | `packages/support/icons/HEROICONS-LICENSE.md` |
10
10
  | [Inter](https://github.com/rsms/inter) © The Inter Project Authors | Default font files (`@navette-js/panels/dist/filament/fonts`) | SIL Open Font License 1.1 | `packages/panels/dist/filament/fonts/INTER-LICENSE.txt` |
package/lib/auth.d.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import type { AuthProvider, TenantProvider } from './index.ts';
2
- /** Empreinte d'un mot de passe (scrypt, sel aléatoire) : `$scrypt$N$r$p$sel$empreinte`. */
2
+ /** Hashes a password with scrypt and a random salt: `$scrypt$N$r$p$salt$hash`. */
3
3
  export declare function hashPassword(password: string): Promise<string>;
4
- /** Compare un mot de passe à une empreinte produite par `hashPassword` (en temps constant). Toute autre empreinte est refusée. */
4
+ /** Checks a password against a `hashPassword` hash in constant time. Any other hash format is rejected. */
5
5
  export declare function verifyPassword(password: string, stored: string | null | undefined): Promise<boolean>;
6
6
  export declare function verifyAgainstDummy(password: string): Promise<void>;
7
- /** Jeton signé à durée de vie limitée : `charge.signature`. `purpose` empêche de réutiliser un jeton pour un autre usage. */
7
+ /** Signed, expiring token: `payload.signature`. `purpose` prevents reusing a token for another use. */
8
8
  export declare function signToken(key: string, purpose: string, payload: Record<string, unknown>, ttlSeconds: number): string;
9
- /** Charge d'un jeton valide et non expiré, sinon `null`. */
9
+ /** Payload of a valid, unexpired token, otherwise `null`. */
10
10
  export declare function readToken(key: string, purpose: string, token: unknown): Record<string, unknown> | null;
11
- /** Empreinte courte d'une valeur secrète (mot de passe enregistré) : un jeton qui la contient meurt quand elle change. */
11
+ /** Short fingerprint of a secret (the stored password hash): a token embedding it dies when the secret changes. */
12
12
  export declare const fingerprint: (value: unknown) => string;
13
- /** Signature des adresses de vérification d'e-mail (comme `URL::temporarySignedRoute` de Laravel). */
13
+ /** Signs an expiring email verification URL. */
14
14
  export declare function signVerificationUrl(key: string, id: string | number, email: string, expires: number): {
15
15
  hash: string;
16
16
  signature: string;
@@ -26,30 +26,30 @@ export type MailMessage = {
26
26
  };
27
27
  outro?: string[];
28
28
  };
29
- /** Envoi d'un e-mail : branchez votre service (SMTP, API…). Le texte est fourni prêt à l'emploi, en lignes. */
29
+ /** Sends an email: plug in your own service (SMTP, API…). The text is provided ready to use, as lines. */
30
30
  export interface Mailer {
31
31
  send(message: MailMessage): void | Promise<void>;
32
32
  }
33
- /** Rendu texte d'un message (comme les notifications « mail » de Laravel). */
33
+ /** Plain-text rendering of a message. */
34
34
  export declare const mailText: (message: MailMessage) => string;
35
- /** Par défaut : le message est écrit dans la console (développement). */
35
+ /** Default mailer: writes the message to the console (development). */
36
36
  export declare const consoleMailer: Mailer;
37
- /** Écrit chaque message sur une ligne JSON d'un fichier (tests, démonstration). */
37
+ /** Appends each message to a file as one JSON line (tests, demos). */
38
38
  export declare const fileMailer: (file: string) => Mailer;
39
- /** Accès à une table : `(table) => knex(table)`. */
39
+ /** Table accessor: `(table) => knex(table)`. */
40
40
  export type TableQuery = (table: string) => any;
41
41
  /**
42
- * Utilisateurs dans une table au format de Laravel (`users` : id, name, email, password, email_verified_at).
43
- * Les mots de passe sont hachés par `hashPassword` (scrypt). Pour accepter d'autres empreintes (bcrypt de Laravel…),
44
- * fournissez `verify(motDePasse, empreinte)` : il est consulté pour toute empreinte qui n'est pas au format scrypt.
42
+ * Users stored in a `users` table (id, name, email, password, email_verified_at).
43
+ * Passwords are hashed with `hashPassword` (scrypt). To accept other hash formats (bcrypt…), pass
44
+ * `verify(password, hash)`: it is called for any stored hash that is not in scrypt format.
45
45
  */
46
46
  export declare function knexAuthProvider(db: TableQuery, options?: {
47
47
  table?: string;
48
48
  verify?: (password: string, hash: string) => boolean | Promise<boolean>;
49
49
  }): AuthProvider;
50
50
  /**
51
- * Locataires (équipes) dans une table, liés aux utilisateurs par une table pivot — comme `teams` et `team_user` de la démo Filament.
52
- * `label` : texte au-dessus du nom dans le menu (« Current team »), comme `HasCurrentTenantLabel`.
51
+ * Tenants (teams) stored in a table and linked to users through a pivot table (`teams` and `team_user` by default).
52
+ * `label`: text shown above the tenant name in the menu ("Current team").
53
53
  */
54
54
  export declare function knexTenantProvider(db: TableQuery, options?: {
55
55
  table?: string;
package/lib/auth.js CHANGED
@@ -1,23 +1,22 @@
1
- // Authentification complète : hachage des mots de passe, jetons signés (réinitialisation, vérification de l'adresse),
2
- // envoi des e-mails, et fournisseurs prêts à l'emploi sur une base knex (utilisateurs, locataires).
1
+ // Authentication building blocks: password hashing, signed tokens (password reset, email verification),
2
+ // mailers, and ready-made knex providers (users, tenants).
3
3
  import { appendFileSync } from 'node:fs';
4
4
  import { createHash, createHmac, randomBytes, scrypt as scryptCallback, timingSafeEqual } from 'node:crypto';
5
- // ------------------------------------------------------------------ mots de passe
6
5
  const SCRYPT_COST = { N: 16384, r: 8, p: 1 };
7
6
  const scrypt = (password, salt, length, options) => new Promise((resolve, reject) => scryptCallback(password, salt, length, { ...options, maxmem: 256 * 1024 * 1024 }, (error, key) => (error ? reject(error) : resolve(key))));
8
- /** Empreinte d'un mot de passe (scrypt, sel aléatoire) : `$scrypt$N$r$p$sel$empreinte`. */
7
+ /** Hashes a password with scrypt and a random salt: `$scrypt$N$r$p$salt$hash`. */
9
8
  export async function hashPassword(password) {
10
9
  const salt = randomBytes(16);
11
10
  const hash = await scrypt(password, salt, 32, SCRYPT_COST);
12
11
  return `$scrypt$${SCRYPT_COST.N}$${SCRYPT_COST.r}$${SCRYPT_COST.p}$${salt.toString('base64url')}$${hash.toString('base64url')}`;
13
12
  }
14
- /** Compare un mot de passe à une empreinte produite par `hashPassword` (en temps constant). Toute autre empreinte est refusée. */
13
+ /** Checks a password against a `hashPassword` hash in constant time. Any other hash format is rejected. */
15
14
  export async function verifyPassword(password, stored) {
16
15
  const parts = String(stored ?? '').split('$');
17
16
  if (parts.length !== 7 || parts[1] !== 'scrypt')
18
17
  return false;
19
18
  const [N, r, p] = parts.slice(2, 5).map(Number);
20
- // Paramètres bornés : une empreinte forgée ne peut pas faire travailler le serveur indéfiniment.
19
+ // Bounded parameters: a forged hash cannot make the server work indefinitely.
21
20
  if (![N, r, p].every(Number.isInteger) || N < 2 || N > 2 ** 20 || r < 1 || r > 32 || p < 1 || p > 16)
22
21
  return false;
23
22
  const expected = Buffer.from(parts[6], 'base64url');
@@ -26,18 +25,17 @@ export async function verifyPassword(password, stored) {
26
25
  const actual = await scrypt(password, Buffer.from(parts[5], 'base64url'), expected.length, { N, r, p });
27
26
  return timingSafeEqual(expected, actual);
28
27
  }
29
- /** Empreinte de référence : vérifiée quand l'adresse est inconnue, pour que la réponse prenne le même temps. */
28
+ /** Verified when the email is unknown, so the response takes the same time (no user enumeration by timing). */
30
29
  let dummyHash = null;
31
30
  export async function verifyAgainstDummy(password) { await verifyPassword(password, await (dummyHash ??= hashPassword(randomBytes(16).toString('hex')))); }
32
- // ------------------------------------------------------------------ jetons signés
33
31
  const sign = (key, value) => createHmac('sha256', key).update(value).digest('base64url');
34
32
  const sameText = (a, b) => { const x = Buffer.from(a), y = Buffer.from(b); return x.length === y.length && timingSafeEqual(x, y); };
35
- /** Jeton signé à durée de vie limitée : `charge.signature`. `purpose` empêche de réutiliser un jeton pour un autre usage. */
33
+ /** Signed, expiring token: `payload.signature`. `purpose` prevents reusing a token for another use. */
36
34
  export function signToken(key, purpose, payload, ttlSeconds) {
37
35
  const body = Buffer.from(JSON.stringify({ ...payload, exp: Math.floor(Date.now() / 1000) + ttlSeconds })).toString('base64url');
38
36
  return `${body}.${sign(key, `${purpose}.${body}`)}`;
39
37
  }
40
- /** Charge d'un jeton valide et non expiré, sinon `null`. */
38
+ /** Payload of a valid, unexpired token, otherwise `null`. */
41
39
  export function readToken(key, purpose, token) {
42
40
  if (typeof token !== 'string' || token.length > 2000)
43
41
  return null;
@@ -52,9 +50,9 @@ export function readToken(key, purpose, token) {
52
50
  return null;
53
51
  }
54
52
  }
55
- /** Empreinte courte d'une valeur secrète (mot de passe enregistré) : un jeton qui la contient meurt quand elle change. */
53
+ /** Short fingerprint of a secret (the stored password hash): a token embedding it dies when the secret changes. */
56
54
  export const fingerprint = (value) => createHash('sha256').update(String(value ?? '')).digest('base64url').slice(0, 22);
57
- /** Signature des adresses de vérification d'e-mail (comme `URL::temporarySignedRoute` de Laravel). */
55
+ /** Signs an expiring email verification URL. */
58
56
  export function signVerificationUrl(key, id, email, expires) {
59
57
  const hash = createHash('sha1').update(email).digest('hex');
60
58
  return { hash, signature: sign(key, `verify-email.${id}.${hash}.${expires}`) };
@@ -63,17 +61,17 @@ export function checkVerificationUrl(key, id, hash, expires, signature) {
63
61
  const at = Number(expires);
64
62
  return Number.isFinite(at) && at > Date.now() / 1000 && typeof signature === 'string' && sameText(signature, sign(key, `verify-email.${id}.${hash}.${at}`));
65
63
  }
66
- /** Rendu texte d'un message (comme les notifications « mail » de Laravel). */
64
+ /** Plain-text rendering of a message. */
67
65
  export const mailText = (message) => [...message.lines, ...(message.action ? ['', `${message.action.label} : ${message.action.url}`, ''] : []), ...(message.outro ?? [])].join('\n');
68
- /** Par défaut : le message est écrit dans la console (développement). */
69
- export const consoleMailer = { send(message) { console.log(`[navette:mail] À ${message.to} — ${message.subject}\n${mailText(message)}`); } };
70
- /** Écrit chaque message sur une ligne JSON d'un fichier (tests, démonstration). */
66
+ /** Default mailer: writes the message to the console (development). */
67
+ export const consoleMailer = { send(message) { console.log(`[navette:mail] To ${message.to} — ${message.subject}\n${mailText(message)}`); } };
68
+ /** Appends each message to a file as one JSON line (tests, demos). */
71
69
  export const fileMailer = (file) => ({ send(message) { appendFileSync(file, JSON.stringify({ at: new Date().toISOString(), ...message }) + '\n'); } });
72
70
  const now = () => new Date().toISOString().slice(0, 19).replace('T', ' ');
73
71
  /**
74
- * Utilisateurs dans une table au format de Laravel (`users` : id, name, email, password, email_verified_at).
75
- * Les mots de passe sont hachés par `hashPassword` (scrypt). Pour accepter d'autres empreintes (bcrypt de Laravel…),
76
- * fournissez `verify(motDePasse, empreinte)` : il est consulté pour toute empreinte qui n'est pas au format scrypt.
72
+ * Users stored in a `users` table (id, name, email, password, email_verified_at).
73
+ * Passwords are hashed with `hashPassword` (scrypt). To accept other hash formats (bcrypt…), pass
74
+ * `verify(password, hash)`: it is called for any stored hash that is not in scrypt format.
77
75
  */
78
76
  export function knexAuthProvider(db, options = {}) {
79
77
  const table = options.table ?? 'users';
@@ -115,8 +113,8 @@ export function knexAuthProvider(db, options = {}) {
115
113
  };
116
114
  }
117
115
  /**
118
- * Locataires (équipes) dans une table, liés aux utilisateurs par une table pivot — comme `teams` et `team_user` de la démo Filament.
119
- * `label` : texte au-dessus du nom dans le menu (« Current team »), comme `HasCurrentTenantLabel`.
116
+ * Tenants (teams) stored in a table and linked to users through a pivot table (`teams` and `team_user` by default).
117
+ * `label`: text shown above the tenant name in the menu ("Current team").
120
118
  */
121
119
  export function knexTenantProvider(db, options = {}) {
122
120
  const table = options.table ?? 'teams', pivot = options.pivot ?? 'team_user';
package/lib/calendar.d.ts CHANGED
@@ -1,17 +1,17 @@
1
- /** Un événement. `start` et `end` sont des dates `YYYY-MM-DD` ou des dates-heures ISO (`2026-11-12T09:30`) ; `end` est inclus. */
1
+ /** A calendar event. `start` and `end` are `YYYY-MM-DD` dates or ISO date-times (`2026-11-12T09:30`); `end` is inclusive. */
2
2
  export type CalendarEvent = {
3
3
  id: string | number;
4
4
  title: string;
5
5
  start: string;
6
6
  end?: string | null;
7
- /** Couleur du panel (`primary`, `success`, `warning`, `danger`, `info`, `gray`, ou une couleur déclarée). */
7
+ /** Panel color (`primary`, `success`, `warning`, `danger`, `info`, `gray`, or a registered color). */
8
8
  color?: string | null;
9
- /** Adresse ouverte au clic. Sans adresse, le clic appelle `onEventClick()` du widget s'il est redéfini. */
9
+ /** URL opened on click. Without one, a click calls the widget's `onEventClick()` if it is overridden. */
10
10
  url?: string | null;
11
11
  openUrlInNewTab?: boolean;
12
- /** Texte de l'infobulle ; par défaut, le titre. */
12
+ /** Tooltip text; defaults to the title. */
13
13
  description?: string | null;
14
- /** Données libres, à disposition de `renderEvent()` et de `onEventClick()`. Elles ne sont pas envoyées au navigateur. */
14
+ /** Free-form data for `renderEvent()` and `onEventClick()`. It is never sent to the browser. */
15
15
  extra?: Record<string, unknown>;
16
16
  };
17
17
  export type CalendarView = 'month' | 'week' | 'list';
@@ -19,21 +19,21 @@ export declare const CALENDAR_VIEWS: CalendarView[];
19
19
  export declare const isDate: (value: unknown) => value is string;
20
20
  export declare const addDays: (day: string, count: number) => string;
21
21
  export declare function addMonths(day: string, count: number): string;
22
- /** Premier jour de la semaine contenant `day` ; `firstDay` : 0 = dimanche, 1 = lundi… */
22
+ /** First day of the week containing `day`; `firstDay`: 0 = Sunday, 1 = Monday… */
23
23
  export declare const startOfWeek: (day: string, firstDay?: number) => string;
24
24
  export declare const startOfMonth: (day: string) => string;
25
25
  export declare const endOfMonth: (day: string) => string;
26
- /** Jour (`YYYY-MM-DD`) d'une date ou d'une date-heure d'événement ; nul si la valeur est illisible. */
26
+ /** Day (`YYYY-MM-DD`) of an event date or date-time; `null` if the value is unreadable. */
27
27
  export declare const dayOf: (value: unknown) => string | null;
28
- /** Heure `HH:MM` d'une date-heure d'événement, nulle pour une date seule. */
28
+ /** `HH:MM` time of an event date-time; `null` for a plain date. */
29
29
  export declare const timeOf: (value: unknown) => string | null;
30
- /** Jours affichés par une vue autour de la date d'ancrage : le mois complété en semaines entières, la semaine, ou le mois (liste). */
30
+ /** Days shown by a view around the anchor date: the month padded to whole weeks, the week, or the month (list). */
31
31
  export declare function visibleRange(view: CalendarView, anchor: string, firstDay?: number): {
32
32
  start: string;
33
33
  end: string;
34
34
  };
35
35
  export declare function daysBetween(start: string, end: string): string[];
36
- /** Événements d'un jour, ceux qui ont commencé plus tôt d'abord, puis par heure et par titre. */
36
+ /** Events on a day: those that started earlier first, then by time and title. */
37
37
  export declare function eventsOn(day: string, events: CalendarEvent[]): CalendarEvent[];
38
38
  export type CalendarLabels = {
39
39
  previous: string;
@@ -50,33 +50,33 @@ export type CalendarModel = {
50
50
  heading: string | null;
51
51
  view: CalendarView;
52
52
  views: CalendarView[];
53
- /** Date d'ancrage et date du jour, `YYYY-MM-DD`. */
53
+ /** Anchor date and today's date, `YYYY-MM-DD`. */
54
54
  anchor: string;
55
55
  today: string;
56
56
  firstDay: number;
57
57
  locale: string;
58
58
  events: CalendarEvent[];
59
59
  maxEventsPerDay: number;
60
- /** Les numéros de jour sont des boutons (`selectDate`). */
60
+ /** Day numbers are buttons (`selectDate`). */
61
61
  datesClickable: boolean;
62
- /** Les événements sans adresse sont des boutons (`selectEvent`). */
62
+ /** Events without a URL are buttons (`selectEvent`). */
63
63
  eventsClickable: boolean;
64
64
  labels: CalendarLabels;
65
- /** Remplace le rendu d'un événement : rendre du HTML déjà échappé, ou `null` pour garder le rendu par défaut. */
65
+ /** Overrides an event's rendering: return already-escaped HTML, or `null` to keep the default. */
66
66
  renderEvent?: (event: CalendarEvent, context: {
67
67
  day: string;
68
68
  view: CalendarView;
69
69
  }) => string | null;
70
- /** HTML ajouté au bas de la case d'un jour (total, pastille…), déjà échappé. */
70
+ /** Already-escaped HTML appended to the bottom of a day cell (total, badge…). */
71
71
  renderDayFooter?: (day: string, events: CalendarEvent[]) => string;
72
- /** HTML placé à droite de la barre d'outils (boutons d'action…), déjà échappé. */
72
+ /** Already-escaped HTML placed at the end of the toolbar (action buttons…). */
73
73
  toolbarEnd?: string;
74
74
  };
75
- /** Rendu par défaut d'un événement : un lien, un bouton ou un simple bloc, selon ce qu'un clic peut faire. */
75
+ /** Default event rendering: a link, a button or a plain block, depending on what a click can do. */
76
76
  export declare function renderCalendarEvent(event: CalendarEvent, context: {
77
77
  day: string;
78
78
  view: CalendarView;
79
79
  clickable: boolean;
80
80
  }): string;
81
- /** Rend le calendrier complet : barre d'outils, puis la vue demandée. */
81
+ /** Renders the whole calendar: toolbar, then the requested view. */
82
82
  export declare function renderCalendar(model: CalendarModel): string;
package/lib/calendar.js CHANGED
@@ -1,5 +1,5 @@
1
- // Calendrier — composant propre à Navette (Filament n'en fournit pas) : dates, grille et rendu, sans dépendance au runtime.
2
- // Le widget `CalendarWidget` (index.ts) s'appuie sur ce module ; chaque morceau du rendu peut être remplacé.
1
+ // Calendar: date helpers, grid and rendering, with no runtime dependency.
2
+ // `CalendarWidget` (index.ts) builds on this module; every part of the rendering can be replaced.
3
3
  import { Attributes, e, icon } from '@navette-js/support';
4
4
  export const CALENDAR_VIEWS = ['month', 'week', 'list'];
5
5
  const DAY = 86400000;
@@ -14,15 +14,15 @@ export function addMonths(day, count) {
14
14
  target.setUTCDate(Math.min(date.getUTCDate(), last));
15
15
  return iso(target);
16
16
  }
17
- /** Premier jour de la semaine contenant `day` ; `firstDay` : 0 = dimanche, 1 = lundi… */
17
+ /** First day of the week containing `day`; `firstDay`: 0 = Sunday, 1 = Monday… */
18
18
  export const startOfWeek = (day, firstDay = 1) => addDays(day, -((toDate(day).getUTCDay() - firstDay + 7) % 7));
19
19
  export const startOfMonth = (day) => `${day.slice(0, 8)}01`;
20
20
  export const endOfMonth = (day) => { const date = toDate(day); return iso(new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0))); };
21
- /** Jour (`YYYY-MM-DD`) d'une date ou d'une date-heure d'événement ; nul si la valeur est illisible. */
21
+ /** Day (`YYYY-MM-DD`) of an event date or date-time; `null` if the value is unreadable. */
22
22
  export const dayOf = (value) => (typeof value === 'string' && isDate(value.slice(0, 10)) ? value.slice(0, 10) : null);
23
- /** Heure `HH:MM` d'une date-heure d'événement, nulle pour une date seule. */
23
+ /** `HH:MM` time of an event date-time; `null` for a plain date. */
24
24
  export const timeOf = (value) => (typeof value === 'string' ? (/^\d{4}-\d{2}-\d{2}[T ](\d{2}:\d{2})/.exec(value)?.[1] ?? null) : null);
25
- /** Jours affichés par une vue autour de la date d'ancrage : le mois complété en semaines entières, la semaine, ou le mois (liste). */
25
+ /** Days shown by a view around the anchor date: the month padded to whole weeks, the week, or the month (list). */
26
26
  export function visibleRange(view, anchor, firstDay = 1) {
27
27
  if (view === 'week') {
28
28
  const start = startOfWeek(anchor, firstDay);
@@ -40,7 +40,7 @@ export function daysBetween(start, end) {
40
40
  days.push(day);
41
41
  return days;
42
42
  }
43
- /** Événements d'un jour, ceux qui ont commencé plus tôt d'abord, puis par heure et par titre. */
43
+ /** Events on a day: those that started earlier first, then by time and title. */
44
44
  export function eventsOn(day, events) {
45
45
  return events
46
46
  .filter((event) => { const start = dayOf(event.start); const end = dayOf(event.end ?? event.start) ?? start; return start !== null && start <= day && day <= (end < start ? start : end); })
@@ -48,10 +48,10 @@ export function eventsOn(day, events) {
48
48
  }
49
49
  export const CALENDAR_LABELS = { previous: 'Previous', next: 'Next', today: 'Today', month: 'Month', week: 'Week', list: 'List', more: (count) => `+${count} more`, empty: 'No events.' };
50
50
  const COLOR = /^[a-z][a-z0-9-]*$/;
51
- /** Seules une adresse du site (`/…`) ou une adresse http(s) deviennent un lien. */
51
+ /** Only a same-site path (`/…`) or an http(s) URL becomes a link (no `javascript:` or protocol-relative URLs). */
52
52
  const safeUrl = (url) => (typeof url === 'string' && /^(\/(?!\/)|https?:\/\/)/.test(url) ? url : null);
53
53
  const call = (method, argument) => `${method}(${JSON.stringify(String(argument)).replace(/"/g, "'")})`;
54
- /** Rendu par défaut d'un événement : un lien, un bouton ou un simple bloc, selon ce qu'un clic peut faire. */
54
+ /** Default event rendering: a link, a button or a plain block, depending on what a click can do. */
55
55
  export function renderCalendarEvent(event, context) {
56
56
  const start = dayOf(event.start);
57
57
  const end = dayOf(event.end ?? event.start) ?? start;
@@ -65,7 +65,7 @@ export function renderCalendarEvent(event, context) {
65
65
  const tag = url ? 'a' : context.clickable ? 'button' : 'div';
66
66
  return `<${tag} ${attributes.toHtml()}>${time ? `<time class="nv-cal-event-time">${e(time)}</time>` : ''}<span class="nv-cal-event-title">${e(event.title)}</span></${tag}>`;
67
67
  }
68
- /** Rend le calendrier complet : barre d'outils, puis la vue demandée. */
68
+ /** Renders the whole calendar: toolbar, then the requested view. */
69
69
  export function renderCalendar(model) {
70
70
  const { view, anchor, today, firstDay, locale, labels } = model;
71
71
  const range = visibleRange(view, anchor, firstDay);