@navette-js/support 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
@@ -2,7 +2,7 @@
2
2
 
3
3
  Shared utilities, colors, icons, views and translations for Navette.
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/i18n.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export declare const setLocale: (locale: string) => void;
2
2
  export declare const getLocale: () => string;
3
- /** `t('filament-panels::auth/pages/login.form.email.label')` */
3
+ /** Translates a key such as `t('filament-panels::auth/pages/login.form.email.label')`; returns the key when it is missing. */
4
4
  export declare function t(key: string, params?: Record<string, string | number>, locale?: string): string;
5
- /** Durée relative au passé, à la manière de Carbon::diffForHumans() en anglais : « 2 months ago ». Les dates sans fuseau sont lues en UTC. */
5
+ /** Relative past duration in English, e.g. "2 months ago". Dates without a time zone are read as UTC. */
6
6
  export declare function diffForHumans(value: string | Date | null | undefined, now?: Date): string | null;
7
- /** Pluriels Laravel (`trans_choice`) : `{0} …|{1} …|[2,*] …` ou `singulier|pluriel`. `:count` est toujours disponible. */
7
+ /** Pluralized translation: `{0} …|{1} …|[2,*] …` or `singular|plural`. `:count` is always available. */
8
8
  export declare function tc(key: string, count: number, params?: Record<string, string | number>, locale?: string): string;
package/lib/i18n.js CHANGED
@@ -1,4 +1,4 @@
1
- // Traductions compatibles Laravel : `espace::groupe.clé`, paramètres `:nom`, repli sur l'anglais.
1
+ // Translations: `namespace::group.key` keys, `:name` placeholders, English fallback.
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  const bundle = JSON.parse(readFileSync(fileURLToPath(new URL('../data/lang.en-fr.json', import.meta.url)), 'utf8'));
@@ -9,7 +9,7 @@ const NAMESPACES = {
9
9
  let currentLocale = 'en';
10
10
  export const setLocale = (locale) => { currentLocale = locale; };
11
11
  export const getLocale = () => currentLocale;
12
- /** `t('filament-panels::auth/pages/login.form.email.label')` */
12
+ /** Translates a key such as `t('filament-panels::auth/pages/login.form.email.label')`; returns the key when it is missing. */
13
13
  export function t(key, params = {}, locale = currentLocale) {
14
14
  const [ns, rest] = key.split('::');
15
15
  const dot = rest.indexOf('.');
@@ -21,7 +21,7 @@ export function t(key, params = {}, locale = currentLocale) {
21
21
  }
22
22
  return value;
23
23
  }
24
- /** Durée relative au passé, à la manière de Carbon::diffForHumans() en anglais : « 2 months ago ». Les dates sans fuseau sont lues en UTC. */
24
+ /** Relative past duration in English, e.g. "2 months ago". Dates without a time zone are read as UTC. */
25
25
  export function diffForHumans(value, now = new Date()) {
26
26
  if (value === null || value === undefined || value === '')
27
27
  return null;
@@ -39,7 +39,7 @@ export function diffForHumans(value, now = new Date()) {
39
39
  const days = Math.floor(seconds / 86400);
40
40
  if (days < 7)
41
41
  return unit(days, 'day');
42
- // Mois calendaires entiers écoulés, comme Carbon.
42
+ // Whole calendar months elapsed, not 30-day blocks.
43
43
  let months = (now.getUTCFullYear() - date.getUTCFullYear()) * 12 + now.getUTCMonth() - date.getUTCMonth();
44
44
  if (now.getUTCDate() < date.getUTCDate() || (now.getUTCDate() === date.getUTCDate() && now.getTime() % 86400000 < date.getTime() % 86400000))
45
45
  months--;
@@ -49,7 +49,7 @@ export function diffForHumans(value, now = new Date()) {
49
49
  return unit(months, 'month');
50
50
  return unit(Math.floor(months / 12), 'year');
51
51
  }
52
- /** Pluriels Laravel (`trans_choice`) : `{0} …|{1} …|[2,*] …` ou `singulier|pluriel`. `:count` est toujours disponible. */
52
+ /** Pluralized translation: `{0} …|{1} …|[2,*] …` or `singular|plural`. `:count` is always available. */
53
53
  export function tc(key, count, params = {}, locale = currentLocale) {
54
54
  const segments = t(key, {}, locale).split('|');
55
55
  let chosen = null;
package/lib/index.d.ts CHANGED
@@ -1,12 +1,13 @@
1
+ /** Escapes `&`, `<`, `>`, `"` and `'` for HTML. */
1
2
  export declare function e(value: unknown): string;
2
- /** Chaîne HTML sûre : n'est pas ré-échappée par les vues. */
3
+ /** Trusted HTML string: views do not escape it again. */
3
4
  export declare class HtmlString {
4
5
  html: string;
5
6
  constructor(html: string);
6
7
  toString(): string;
7
8
  }
8
9
  export declare const raw: (html: string) => HtmlString;
9
- /** Sérialise une valeur en expression JavaScript, au même format que `Js::from()` de Laravel. */
10
+ /** Serializes a value into a JavaScript expression that is safe to embed in HTML. */
10
11
  export declare function js(value: unknown): string;
11
12
  export type ClassList = (string | false | null | undefined | Record<string, unknown>)[] | Record<string, unknown>;
12
13
  export declare function cls(list: ClassList): string;
@@ -16,20 +17,20 @@ export declare class Attributes {
16
17
  constructor(initial?: Record<string, AttrValue>);
17
18
  get(key: string): AttrValue;
18
19
  has(key: string): boolean;
19
- /** Port de merge() : les valeurs par défaut passent en premier, puis les attributs existants ; null/false sont ignorés. */
20
+ /** Defaults come first, then existing attributes (`class` and `style` are concatenated); null/false defaults are skipped. */
20
21
  merge(defaults: Record<string, AttrValue>): Attributes;
21
- /** Ajoute (ou remplace) un attribut en fin de liste. */
22
+ /** Adds (or replaces) an attribute at the end. */
22
23
  set(key: string, value: AttrValue): Attributes;
23
24
  private clone;
24
- /** Port de class() : les classes s'ajoutent après les classes existantes ; l'attribut garde sa place, ou va en fin. */
25
+ /** Appends classes after the existing ones; the attribute keeps its position, or goes last. */
25
26
  class(list: ClassList): Attributes;
26
27
  style(list: (string | false | null | undefined)[]): Attributes;
27
28
  when(condition: unknown, fn: (a: Attributes) => Attributes): Attributes;
28
- /** Grille conteneur : port de ComponentAttributeBag::grid(). */
29
+ /** Grid container: `fi-grid` classes and `--cols-*` variables per breakpoint. */
29
30
  grid(columns?: Record<string, number> | number | null): Attributes;
30
- /** Colonne de grille : port de ComponentAttributeBag::gridColumn(). */
31
+ /** Grid column: `fi-grid-col` classes and `--col-span-*` variables per breakpoint. */
31
32
  gridColumn(span?: Record<string, number | string> | number | string | null, isHidden?: boolean): Attributes;
32
- /** Rendu `clé="valeur"` ; `true` devient `clé="clé"` (ou `""` pour x-data et wire:*), comme dans Laravel. Les valeurs ne sont pas ré-échappées. */
33
+ /** Renders `key="value"` pairs; `true` becomes `key="key"` (or `""` for x-data and wire:*). Values are not escaped again, except `"`. */
33
34
  toHtml(): string;
34
35
  toString(): string;
35
36
  }
@@ -39,39 +40,39 @@ export declare function evaluate<T, C>(value: Lazy<T, C>, ctx: C): Promise<T>;
39
40
  export type Palette = Record<number, string>;
40
41
  export declare const Color: Record<string, Palette>;
41
42
  export declare const WCAG_AA_TEXT = 4.5;
42
- /** Port de Color::convertToOklch : `#rrggbb`, `rgb(r, g, b)` ou `r, g, b` → `oklch(L C h)`, arrondi à trois décimales. */
43
+ /** Converts `#rrggbb`, `rgb(r, g, b)` or `r, g, b` to `oklch(L C h)`, rounded to three decimals. */
43
44
  export declare function convertToOklch(color: string): string;
44
- /** Port de Color::generatePalette (`Color::hex()`, `Color::rgb()`) : onze nuances à partir d'une seule couleur, dont on garde la teinte. */
45
+ /** Builds an eleven-shade palette from a single color, keeping its hue. */
45
46
  export declare function generatePalette(color: string): Palette;
46
- /** Une palette toute faite, ou une couleur (`#6366f1`, `rgb(…)`) dont la palette est générée. */
47
+ /** A ready-made palette, or a color (`#6366f1`, `rgb(…)`) from which a palette is generated. */
47
48
  export type ColorInput = Palette | string;
48
49
  export declare function convertToRgb(color: string): [number, number, number];
49
- /** Rapport de contraste de Filament : luminance approchée par un gamma 2,2 (reproduit tel quel, ce n'est pas la formule WCAG exacte). */
50
+ /** Contrast ratio with luminance approximated by a 2.2 gamma (intentionally not the exact WCAG formula; shade choices depend on it). */
50
51
  export declare function calculateContrastRatio(c1: string, c2: string): number;
51
52
  export declare function isLight(color: string): boolean;
52
- /** Port de ButtonComponentColorMap : choisit les nuances de fond et de texte accessibles d'un bouton plein. */
53
+ /** Picks accessible background and text shades for a filled button. */
53
54
  export declare function buttonColorMap(palette: Palette, minRatio?: number): Record<string, number>;
54
- /** Port de Color::findShade : première nuance de la palette assez contrastée sur une surface. */
55
+ /** First shade of the palette with enough contrast against a surface. */
55
56
  export declare function findShade(palette: Palette, surface: string, options?: {
56
57
  minRatio?: number;
57
58
  maxShade?: number;
58
59
  minShade?: number;
59
60
  fromDarkest?: boolean;
60
61
  }): number | null;
61
- /** Cartes de couleurs des composants à texte coloré (BadgeComponent, LinkComponent). */
62
+ /** Color map of badges. */
62
63
  export declare function badgeColorMap(palette: Palette, gray: Palette): Record<string, number>;
63
- /** Port de DropdownComponent\ItemComponent::getColorMap. */
64
+ /** Color map of dropdown items. */
64
65
  export declare function dropdownItemColorMap(palette: Palette, gray: Palette): Record<string, number>;
65
- /** Port de IconColumnComponent\IconComponent::getColorMap (contraste non textuel, 3:1). */
66
+ /** Color map of icon columns (non-text contrast, 3:1). */
66
67
  export declare function iconColumnColorMap(palette: Palette, gray: Palette): Record<string, number>;
67
- /** Port de IconButtonComponentColorMap (contraste non textuel 3:1, surfaces gray-50 / gray-700). */
68
+ /** Color map of icon buttons (non-text contrast 3:1 on gray-50 / gray-700). */
68
69
  export declare function iconButtonColorMap(palette: Palette, gray: Palette): Record<string, number>;
69
- /** Port de ToggleComponent::getColorMap (contraste non textuel 3:1). */
70
+ /** Color map of toggles (non-text contrast, 3:1). */
70
71
  export declare function toggleColorMap(palette: Palette, gray: Palette): Record<string, number>;
71
72
  export declare function linkColorMap(palette: Palette, gray: Palette): Record<string, number>;
72
- /** Bouton à contour : seule la couleur du texte dépend de la palette (ButtonComponent, `isOutlined`). */
73
+ /** Outlined button: only the text color depends on the palette. */
73
74
  export declare function outlinedButtonColorMap(palette: Palette, gray: Palette): Record<string, number>;
74
- /** Carte de couleurs de chaque composant coloré (équivalent des classes `*Component::getColorMap`). */
75
+ /** Color map of each colored component. */
75
76
  declare const COMPONENT_COLOR_MAPS: {
76
77
  button: (palette: Palette) => Record<string, number>;
77
78
  'button-outlined': typeof outlinedButtonColorMap;
@@ -92,73 +93,73 @@ declare const COMPONENT_COLOR_MAPS: {
92
93
  };
93
94
  };
94
95
  export type ColoredComponent = keyof typeof COMPONENT_COLOR_MAPS;
95
- /** Port de ColorManager. */
96
+ /** Registers named palettes and computes the color classes of components. */
96
97
  export declare class ColorManager {
97
98
  static DEFAULT_COLORS: Record<string, Palette>;
98
99
  private colors;
99
100
  private cache;
100
- /** Couleurs propres à la requête en cours (thème choisi par l'utilisateur) : elles priment sur celles du panel. */
101
+ /** Per-request colors (theme picked by the user); they take precedence over the panel's colors. */
101
102
  private scope;
102
103
  register(colors: Record<string, ColorInput>): this;
103
- /** Exécute `fn` avec des couleurs qui ne valent que pour cette requête ; `key` identifie le jeu de couleurs (cache). */
104
+ /** Runs `fn` with colors that only apply to this request; `key` identifies the color set for caching. */
104
105
  runWith<T>(key: string, colors: Record<string, ColorInput>, fn: () => T): T;
105
106
  getColors(): Record<string, Palette>;
106
107
  getColor(name: string): Palette | undefined;
107
- /** Classes d'un composant coloré (`getComponentClasses`). Seul le bouton plein est porté pour l'instant. */
108
+ /** Color classes of a colored component. */
108
109
  getComponentClasses(component: ColoredComponent, color: string | null): string[];
109
- /** Bloc `<style>` des variables `--<couleur>-<nuance>` injecté dans `<head>`. */
110
+ /** `<style>` block defining the `--<color>-<shade>` variables, injected into `<head>`. */
110
111
  renderCssVariables(): string;
111
112
  }
112
- /** Icône Heroicons (`m-eye`, `o-user`, `s-…`, `c-…`) avec les classes `fi-icon fi-size-<taille>`. */
113
+ /** Heroicons icon (`m-eye`, `o-user`, `s-…`, `c-…`) with the `fi-icon fi-size-<size>` classes. */
113
114
  export declare function icon(name: string, options?: {
114
115
  size?: string;
115
116
  attributes?: Attributes;
116
117
  }): string;
117
- /** Déclare une icône hors Heroicons (logo de marque, pictogramme maison) : un `<svg>` complet, fourni par l'application et non par l'utilisateur. */
118
+ /** Registers a non-Heroicons icon (brand logo, custom glyph): a full `<svg>` supplied by the application, never by end users (it is not sanitized). */
118
119
  export declare function registerIcon(name: string, svg: string): void;
119
- /** Indicateur de chargement (`generate_loading_indicator_html`). */
120
+ /** Loading spinner SVG. */
120
121
  export declare function loadingIndicator(attributes?: Attributes, size?: string): string;
121
- /** Registre de vues : `filament-forms::components.text-input` → fichier EJS ; les dossiers de surcharge passent en premier. */
122
+ /** View registry: `filament-forms::components.text-input` resolves to an EJS file; override directories are checked first. */
122
123
  export declare class ViewRegistry {
123
124
  private namespaces;
124
125
  private overrides;
125
126
  private compiled;
126
127
  private shared;
127
128
  namespace(name: string, dir: string): this;
128
- /** Dossier de surcharge de l'application : `<dir>/<espace>/<chemin>.ejs` remplace la vue du paquet. */
129
+ /** Application override directory: `<dir>/<namespace>/<path>.ejs` replaces the package view. */
129
130
  overrideFrom(dir: string): this;
130
131
  share(data: Record<string, unknown>): this;
131
132
  resolve(view: string): string;
132
133
  render(view: string, data?: Record<string, unknown>): string;
133
134
  }
134
135
  export declare const views: ViewRegistry;
135
- /** Enregistre un réglage appliqué à chaque instance de la classe (et de ses sous-classes) créée ensuite par `make()`. */
136
+ /** Registers a callback applied to every instance of the class (and its subclasses) created afterwards by `make()`. */
136
137
  export declare function configureUsing<T>(cls: abstract new (...args: any[]) => T, configure: (instance: T) => void): void;
137
- /** Applique les réglages globaux à une instance fraîchement créée, de la classe la plus générale à la plus précise. */
138
+ /** Applies the global callbacks to a new instance, from the most general class to the most specific. */
138
139
  export declare function configured<T extends object>(instance: T): T;
139
- /** Oublie tous les réglages globaux (tests). */
140
+ /** Forgets all global callbacks (for tests). */
140
141
  export declare function resetConfigurators(): void;
141
- /** Ce qu'un render hook reçoit : les portées de l'appel (page, resource…) et les données de l'emplacement (`action`, `column`…). */
142
+ /** What a render hook receives: the call's scopes (page, resource…) and the slot's data (`action`, `column`…). */
142
143
  export type RenderHookContext = {
143
144
  scopes: unknown[];
144
145
  data: Record<string, any>;
145
146
  };
146
147
  export type RenderHookCallback = (context: RenderHookContext) => string | null | undefined;
147
- /** Registre de render hooks. Un hook sans portée est rendu partout ; avec portées, seulement quand l'appel porte l'une d'elles. */
148
+ /** Render hook registry. Unscoped hooks render everywhere; scoped hooks only when the call carries one of their scopes. */
148
149
  export declare class RenderHookRegistry {
149
150
  private hooks;
150
151
  register(name: string, hook: RenderHookCallback, scopes?: unknown | unknown[] | null): this;
151
- /** Hooks à rendre pour cet appel, dans l'ordre de Filament : sans portée d'abord, puis portée par portée, chacun une seule fois. */
152
+ /** Hooks to render for a call: unscoped first, then scope by scope, each hook once. */
152
153
  collect(name: string, scopes: unknown[]): RenderHookCallback[];
153
154
  has(name: string, scopes?: unknown | unknown[] | null): boolean;
154
155
  render(name: string, scopes?: unknown | unknown[] | null, data?: Record<string, any>): string;
155
156
  clear(): void;
156
157
  }
157
- /** Hooks communs à tout le processus (tous les panels). */
158
+ /** Process-wide hooks (all panels). */
158
159
  export declare const renderHooks: RenderHookRegistry;
159
- /** Exécute un rendu avec, en plus des hooks communs, ceux d'un registre propre (celui du panel de la requête). */
160
+ /** Renders with an extra registry (the request panel's) on top of the process-wide hooks. */
160
161
  export declare function runWithRenderHooks<T>(registry: RenderHookRegistry, fn: () => T): T;
161
- /** Rend un emplacement : hooks communs, puis hooks du panel en cours. */
162
+ /** Renders a slot: process-wide hooks, then the current panel's hooks. */
162
163
  export declare function renderHook(name: string, scopes?: unknown | unknown[] | null, data?: Record<string, any>): string;
163
164
  export declare function hasRenderHook(name: string, scopes?: unknown | unknown[] | null): boolean;
164
165
  export {};
package/lib/index.js CHANGED
@@ -1,23 +1,24 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
- // @navette-js/support — briques transverses portées de filament/support : échappement, sac d'attributs, sérialisation JS,
3
- // évaluation paresseuse, couleurs, icônes, vues EJS à espaces de noms.
2
+ // @navette-js/support — shared building blocks: escaping, attribute bags, JS serialization, lazy evaluation, colors, icons,
3
+ // namespaced EJS views.
4
4
  import ejs from 'ejs';
5
5
  import { existsSync, readFileSync } from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
- // ------------------------------------------------------------------ échappement (équivalent de e() / htmlspecialchars)
8
+ // ------------------------------------------------------------------ escaping
9
+ /** Escapes `&`, `<`, `>`, `"` and `'` for HTML. */
9
10
  export function e(value) {
10
11
  return String(value ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
11
12
  }
12
- /** Chaîne HTML sûre : n'est pas ré-échappée par les vues. */
13
+ /** Trusted HTML string: views do not escape it again. */
13
14
  export class HtmlString {
14
15
  html;
15
16
  constructor(html) { this.html = html; }
16
17
  toString() { return this.html; }
17
18
  }
18
19
  export const raw = (html) => new HtmlString(html);
19
- // ------------------------------------------------------------------ Js::from (Illuminate\Support\Js)
20
- // Chaîne JS entre apostrophes, comme json_encode(JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) de PHP.
20
+ // ------------------------------------------------------------------ JS serialization
21
+ // Quotes, `<`, `>` and `&` are written as `\uXXXX` so the output is safe inside HTML attributes and `<script>`.
21
22
  const BS = String.fromCharCode(92);
22
23
  const HEX = { '"': 'u0022', "'": 'u0027', '<': 'u003C', '>': 'u003E', '&': 'u0026', '/': '/' };
23
24
  function phpString(value) {
@@ -34,10 +35,7 @@ function phpString(value) {
34
35
  }
35
36
  return out;
36
37
  }
37
- /**
38
- * `json_encode($value, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT)` : dans les chaînes, `'`, `"`, `<`, `>` et `&`
39
- * sont écrits en `\uXXXX` (les guillemets de structure restent tels quels), et `/` est échappé.
40
- */
38
+ /** JSON where `'`, `"`, `<`, `>` and `&` inside strings become `\uXXXX` (structural quotes are kept) and `/` is escaped. */
41
39
  function phpJson(value) {
42
40
  const json = JSON.stringify(value);
43
41
  const HEX = { "'": BS + 'u0027', '<': BS + 'u003C', '>': BS + 'u003E', '&': BS + 'u0026', '/': BS + '/' };
@@ -53,7 +51,7 @@ function phpJson(value) {
53
51
  }
54
52
  return out;
55
53
  }
56
- /** Sérialise une valeur en expression JavaScript, au même format que `Js::from()` de Laravel. */
54
+ /** Serializes a value into a JavaScript expression that is safe to embed in HTML. */
57
55
  export function js(value) {
58
56
  if (value === null || value === undefined)
59
57
  return 'null';
@@ -84,7 +82,7 @@ export class Attributes {
84
82
  this.map.set(k, v); }
85
83
  get(key) { return this.map.get(key); }
86
84
  has(key) { return this.map.has(key); }
87
- /** Port de merge() : les valeurs par défaut passent en premier, puis les attributs existants ; null/false sont ignorés. */
85
+ /** Defaults come first, then existing attributes (`class` and `style` are concatenated); null/false defaults are skipped. */
88
86
  merge(defaults) {
89
87
  const out = new Attributes();
90
88
  for (const [k, v] of Object.entries(defaults)) {
@@ -99,11 +97,11 @@ export class Attributes {
99
97
  }
100
98
  return out;
101
99
  }
102
- /** Ajoute (ou remplace) un attribut en fin de liste. */
100
+ /** Adds (or replaces) an attribute at the end. */
103
101
  set(key, value) { const out = this.clone(); out.map.delete(key); out.map.set(key, value); return out; }
104
102
  clone() { const out = new Attributes(); for (const [k, v] of this.map)
105
103
  out.map.set(k, v); return out; }
106
- /** Port de class() : les classes s'ajoutent après les classes existantes ; l'attribut garde sa place, ou va en fin. */
104
+ /** Appends classes after the existing ones; the attribute keeps its position, or goes last. */
107
105
  class(list) {
108
106
  const out = this.clone();
109
107
  out.map.set('class', [out.map.get('class'), cls(list)].filter((x) => x !== '' && x != null).join(' '));
@@ -119,7 +117,7 @@ export class Attributes {
119
117
  return out;
120
118
  }
121
119
  when(condition, fn) { return condition ? fn(this) : this; }
122
- /** Grille conteneur : port de ComponentAttributeBag::grid(). */
120
+ /** Grid container: `fi-grid` classes and `--cols-*` variables per breakpoint. */
123
121
  grid(columns = {}) {
124
122
  const cols = typeof columns === 'number' ? { lg: columns } : { ...(columns ?? {}) };
125
123
  for (const k of Object.keys(cols))
@@ -131,7 +129,7 @@ export class Attributes {
131
129
  .class(['fi-grid', ...Object.keys(cols).map((bp) => (bp === 'default' ? (cols[bp] > 1 ? 'fi-grid-cols' : '') : `${bp}:fi-grid-cols`))])
132
130
  .style(Object.entries(cols).map(([bp, n]) => `--cols-${v(bp)}: repeat(${n}, minmax(0, 1fr))`));
133
131
  }
134
- /** Colonne de grille : port de ComponentAttributeBag::gridColumn(). */
132
+ /** Grid column: `fi-grid-col` classes and `--col-span-*` variables per breakpoint. */
135
133
  gridColumn(span = {}, isHidden = false) {
136
134
  const s = span !== null && typeof span === 'object' ? { ...span } : span ? { lg: span } : {};
137
135
  for (const k of Object.keys(s))
@@ -142,7 +140,7 @@ export class Attributes {
142
140
  .class(['fi-grid-col', { 'fi-hidden': isHidden || s.default === 'hidden' }, ...Object.keys(s).map((bp) => (bp === 'default' ? '' : `${bp}:fi-grid-col-span`))])
143
141
  .style(Object.entries(s).map(([bp, n]) => `--col-span-${v(bp)}: ${n === 'full' ? '1 / -1' : `span ${n} / span ${n}`}`));
144
142
  }
145
- /** Rendu `clé="valeur"` ; `true` devient `clé="clé"` (ou `""` pour x-data et wire:*), comme dans Laravel. Les valeurs ne sont pas ré-échappées. */
143
+ /** Renders `key="value"` pairs; `true` becomes `key="key"` (or `""` for x-data and wire:*). Values are not escaped again, except `"`. */
146
144
  toHtml() {
147
145
  const parts = [];
148
146
  for (const [k, v] of this.map) {
@@ -159,11 +157,11 @@ export const attrs = (initial = {}) => new Attributes(initial);
159
157
  export async function evaluate(value, ctx) {
160
158
  return typeof value === 'function' ? await value(ctx) : value;
161
159
  }
162
- // ------------------------------------------------------------------ couleurs (Filament\Support\Colors)
160
+ // ------------------------------------------------------------------ colors
163
161
  const here = (p) => fileURLToPath(new URL(p, import.meta.url));
164
162
  export const Color = JSON.parse(readFileSync(here('../data/palettes.json'), 'utf8'));
165
163
  export const WCAG_AA_TEXT = 4.5;
166
- /** Port de Color::convertToOklch : `#rrggbb`, `rgb(r, g, b)` ou `r, g, b` → `oklch(L C h)`, arrondi à trois décimales. */
164
+ /** Converts `#rrggbb`, `rgb(r, g, b)` or `r, g, b` to `oklch(L C h)`, rounded to three decimals. */
167
165
  export function convertToOklch(color) {
168
166
  if (color.startsWith('oklch('))
169
167
  return color;
@@ -173,7 +171,7 @@ export function convertToOklch(color) {
173
171
  return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
174
172
  });
175
173
  if (![r, g, b].every(Number.isFinite))
176
- throw new Error(`Couleur non prise en charge : ${color}`);
174
+ throw new Error(`Unsupported color: ${color}`);
177
175
  const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b), m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b), s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
178
176
  const lightness = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;
179
177
  const a = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, bb = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
@@ -183,14 +181,14 @@ export function convertToOklch(color) {
183
181
  const round = (n) => String(Math.round(n * 1000) / 1000);
184
182
  return `oklch(${round(lightness)} ${round(Math.sqrt(a * a + bb * bb))} ${round(hue)})`;
185
183
  }
186
- /** Clarté et saturation de chaque nuance d'une palette générée (constantes de Color::generatePalette). */
184
+ /** Lightness and chroma of each shade of a generated palette. */
187
185
  const GENERATED_SHADES = [
188
186
  [50, '0.97717647058824', '0.01395454545455'], [100, '0.95035294117647', '0.03272727272727'], [200, '0.90547058823529', '0.06318181818182'],
189
187
  [300, '0.84047058823529', '0.10604545454546'], [400, '0.75352941176471', '0.15027272727273'], [500, '0.68270588235294', '0.17009090909091'],
190
188
  [600, '0.59782352941176', '0.16913636363636'], [700, '0.51494117647059', '0.14940909090909'], [800, '0.44611764705882', '0.12331818181818'],
191
189
  [900, '0.39458823529412', '0.09963636363636'], [950, '0.27788235294118', '0.07136363636364'],
192
190
  ];
193
- /** Port de Color::generatePalette (`Color::hex()`, `Color::rgb()`) : onze nuances à partir d'une seule couleur, dont on garde la teinte. */
191
+ /** Builds an eleven-shade palette from a single color, keeping its hue. */
194
192
  export function generatePalette(color) {
195
193
  const [, , chroma, hue] = /^oklch\(([\d.]+) ([\d.]+) ([\d.]+)\)$/.exec(convertToOklch(color)).map(Number);
196
194
  const achromatic = chroma < 0.03;
@@ -207,14 +205,14 @@ export function convertToRgb(color) {
207
205
  }
208
206
  const m = /^oklch\(([\d.]+)\s+([\d.]+)(?:\s+([\d.]+))?\)/.exec(color);
209
207
  if (!m)
210
- throw new Error(`Couleur non prise en charge : ${color}`);
208
+ throw new Error(`Unsupported color: ${color}`);
211
209
  const L = +m[1], C = +m[2], h = ((+m[3] || 0) * Math.PI) / 180;
212
210
  const a = C * Math.cos(h), b = C * Math.sin(h);
213
211
  const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3, md = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3, s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
214
212
  const lin = [4.0767416621 * l - 3.3077115913 * md + 0.2309699292 * s, -1.2684380046 * l + 2.6097574011 * md - 0.3413193965 * s, -0.0041960863 * l - 0.7034186147 * md + 1.707614701 * s];
215
213
  return lin.map((x) => Math.max(0, Math.min(255, Math.round((x <= 0.0031308 ? 12.92 * x : 1.055 * x ** (1 / 2.4) - 0.055) * 255))));
216
214
  }
217
- /** Rapport de contraste de Filament : luminance approchée par un gamma 2,2 (reproduit tel quel, ce n'est pas la formule WCAG exacte). */
215
+ /** Contrast ratio with luminance approximated by a 2.2 gamma (intentionally not the exact WCAG formula; shade choices depend on it). */
218
216
  export function calculateContrastRatio(c1, c2) {
219
217
  const lum = (c) => { const [r, g, b] = convertToRgb(c); return 0.2126 * (r / 255) ** 2.2 + 0.7152 * (g / 255) ** 2.2 + 0.0722 * (b / 255) ** 2.2; };
220
218
  const a = lum(c1), b = lum(c2);
@@ -223,10 +221,10 @@ export function calculateContrastRatio(c1, c2) {
223
221
  export function isLight(color) {
224
222
  const m = /^oklch\(([\d.]+)/.exec(color);
225
223
  if (!m)
226
- throw new Error(`isLight : seule la notation oklch est prise en charge pour l'instant (${color})`);
224
+ throw new Error(`isLight: only oklch colors are supported (${color})`);
227
225
  return +m[1] >= 0.65;
228
226
  }
229
- /** Port de ButtonComponentColorMap : choisit les nuances de fond et de texte accessibles d'un bouton plein. */
227
+ /** Picks accessible background and text shades for a filled button. */
230
228
  export function buttonColorMap(palette, minRatio = WCAG_AA_TEXT) {
231
229
  const light = [{ bg: 600, hover: 500, alternateHover: null }, { bg: 400, hover: 300, alternateHover: 500 }];
232
230
  const dark = [{ bg: 600, hover: 500, alternateHover: 700 }];
@@ -275,7 +273,7 @@ export function buttonColorMap(palette, minRatio = WCAG_AA_TEXT) {
275
273
  const [bg, hoverBg] = pick(light), [darkBg, darkHoverBg] = pick(dark);
276
274
  return { bg, 'hover:bg': hoverBg, 'dark:bg': darkBg, 'dark:hover:bg': darkHoverBg, text: textForBg[bg] ?? 0, 'hover:text': textForBg[hoverBg] ?? 0, 'dark:text': textForBg[darkBg] ?? 0, 'dark:hover:text': textForBg[darkHoverBg] ?? 0 };
277
275
  }
278
- /** Port de Color::findShade : première nuance de la palette assez contrastée sur une surface. */
276
+ /** First shade of the palette with enough contrast against a surface. */
279
277
  export function findShade(palette, surface, options = {}) {
280
278
  const shades = Object.keys(palette).map(Number).sort((a, b) => (options.fromDarkest ? b - a : a - b));
281
279
  for (const shade of shades) {
@@ -288,11 +286,11 @@ export function findShade(palette, surface, options = {}) {
288
286
  }
289
287
  return null;
290
288
  }
291
- /** Cartes de couleurs des composants à texte coloré (BadgeComponent, LinkComponent). */
289
+ /** Color map of badges. */
292
290
  export function badgeColorMap(palette, gray) {
293
291
  return { text: findShade(palette, palette[50]) ?? 900, 'dark:text': findShade(palette, gray[600], { maxShade: 500, fromDarkest: true }) ?? 200 };
294
292
  }
295
- /** Port de DropdownComponent\ItemComponent::getColorMap. */
293
+ /** Color map of dropdown items. */
296
294
  export function dropdownItemColorMap(palette, gray) {
297
295
  return {
298
296
  text: findShade(palette, 'oklch(1 0 0)', { minShade: 600 }) ?? 950,
@@ -301,30 +299,30 @@ export function dropdownItemColorMap(palette, gray) {
301
299
  'dark:hover:text': findShade(palette, palette[950], { maxShade: 400, fromDarkest: true }) ?? 100,
302
300
  };
303
301
  }
304
- /** Port de IconColumnComponent\IconComponent::getColorMap (contraste non textuel, 3:1). */
302
+ /** Color map of icon columns (non-text contrast, 3:1). */
305
303
  export function iconColumnColorMap(palette, gray) {
306
304
  return { text: findShade(palette, 'oklch(1 0 0)', { minRatio: 3 }) ?? 900, 'dark:text': findShade(palette, gray[800], { minRatio: 3, maxShade: 600, fromDarkest: true }) ?? 200 };
307
305
  }
308
- /** Port de IconButtonComponentColorMap (contraste non textuel 3:1, surfaces gray-50 / gray-700). */
306
+ /** Color map of icon buttons (non-text contrast 3:1 on gray-50 / gray-700). */
309
307
  export function iconButtonColorMap(palette, gray) {
310
308
  const light = findShade(palette, gray[50], { minRatio: 3 });
311
309
  const dark = findShade(palette, gray[700], { minRatio: 3, maxShade: 500, fromDarkest: true });
312
- // Au-delà de 500 la teinte est déjà sombre : au survol on l'assombrit au lieu de l'éclaircir.
310
+ // Above 500 the shade is already dark: hover darkens it instead of lightening it.
313
311
  const [text, hoverText] = light === null ? [900, 800] : light > 500 ? [light, light + 100] : [light + 100, light];
314
312
  return { text, 'hover:text': hoverText, 'dark:text': dark ?? 200, 'dark:hover:text': dark === null ? 100 : dark - 100 };
315
313
  }
316
- /** Port de ToggleComponent::getColorMap (contraste non textuel 3:1). */
314
+ /** Color map of toggles (non-text contrast, 3:1). */
317
315
  export function toggleColorMap(palette, gray) {
318
316
  return { bg: findShade(palette, gray[50], { minRatio: 3 }) ?? 900, text: findShade(palette, 'oklch(1 0 0)', { minRatio: 3 }) ?? 900, 'dark:bg': findShade(palette, gray[700], { minRatio: 3, fromDarkest: true }) ?? 200 };
319
317
  }
320
318
  export function linkColorMap(palette, gray) {
321
319
  return { text: findShade(palette, gray[50]) ?? 900, 'dark:text': findShade(palette, gray[700], { maxShade: 400, fromDarkest: true }) ?? 200 };
322
320
  }
323
- /** Bouton à contour : seule la couleur du texte dépend de la palette (ButtonComponent, `isOutlined`). */
321
+ /** Outlined button: only the text color depends on the palette. */
324
322
  export function outlinedButtonColorMap(palette, gray) {
325
323
  return { text: findShade(palette, gray[50]) ?? 900, 'dark:text': findShade(palette, gray[700], { maxShade: 500, fromDarkest: true }) ?? 200 };
326
324
  }
327
- /** Carte de couleurs de chaque composant coloré (équivalent des classes `*Component::getColorMap`). */
325
+ /** Color map of each colored component. */
328
326
  const COMPONENT_COLOR_MAPS = {
329
327
  button: (palette) => buttonColorMap(palette),
330
328
  'button-outlined': outlinedButtonColorMap,
@@ -334,32 +332,32 @@ const COMPONENT_COLOR_MAPS = {
334
332
  'icon-column': iconColumnColorMap,
335
333
  'icon-button': iconButtonColorMap,
336
334
  toggle: toggleColorMap,
337
- // Port de StatsOverviewWidgetComponentStatComponentDescriptionComponent et ChartComponent (ce dernier ne pose que `fi-color`).
335
+ // Stat description and stat chart (the chart only gets `fi-color`).
338
336
  'stat-description': (palette, gray) => ({ text: findShade(palette, 'oklch(1 0 0)', { minShade: 600 }) ?? 950, 'dark:text': findShade(palette, gray[900], { maxShade: 400, fromDarkest: true }) ?? 200 }),
339
337
  'stat-chart': () => ({}),
340
- // Texte coloré d'une colonne ou d'une entrée. Gris : 500 / 400, relevé sur Filament ; autres couleurs : même recherche de contraste que la description d'une statistique (non comparé à Filament).
338
+ // Colored text of a column or entry: fixed 500 / 400 for gray, otherwise the same contrast search as stat descriptions.
341
339
  text: (palette, gray) => (palette === gray ? { text: 500, 'dark:text': 400 } : { text: findShade(palette, 'oklch(1 0 0)', { minShade: 600 }) ?? 950, 'dark:text': findShade(palette, gray[900], { maxShade: 400, fromDarkest: true }) ?? 50 }),
342
340
  };
343
341
  const CLASS_FOR = {
344
342
  bg: (s) => `fi-bg-color-${s}`, 'hover:bg': (s) => `hover:fi-bg-color-${s}`, 'dark:bg': (s) => `dark:fi-bg-color-${s}`, 'dark:hover:bg': (s) => `dark:hover:fi-bg-color-${s}`,
345
343
  text: (s) => `fi-text-color-${s}`, 'hover:text': (s) => `hover:fi-text-color-${s}`, 'dark:text': (s) => `dark:fi-text-color-${s}`, 'dark:hover:text': (s) => `dark:hover:fi-text-color-${s}`,
346
344
  };
347
- /** Port de ColorManager. */
345
+ /** Registers named palettes and computes the color classes of components. */
348
346
  export class ColorManager {
349
347
  static DEFAULT_COLORS = { danger: Color.Red, gray: Color.Zinc, info: Color.Blue, primary: Color.Amber, success: Color.Green, warning: Color.Amber };
350
348
  colors = { ...ColorManager.DEFAULT_COLORS };
351
349
  cache = new Map();
352
- /** Couleurs propres à la requête en cours (thème choisi par l'utilisateur) : elles priment sur celles du panel. */
350
+ /** Per-request colors (theme picked by the user); they take precedence over the panel's colors. */
353
351
  scope = new AsyncLocalStorage();
354
352
  register(colors) { for (const [name, color] of Object.entries(colors))
355
353
  this.colors[name] = toPalette(color); this.cache.clear(); return this; }
356
- /** Exécute `fn` avec des couleurs qui ne valent que pour cette requête ; `key` identifie le jeu de couleurs (cache). */
354
+ /** Runs `fn` with colors that only apply to this request; `key` identifies the color set for caching. */
357
355
  runWith(key, colors, fn) {
358
356
  return this.scope.run({ key, colors: Object.fromEntries(Object.entries(colors).map(([name, color]) => [name, toPalette(color)])) }, fn);
359
357
  }
360
358
  getColors() { return { ...this.colors, ...(this.scope.getStore()?.colors ?? {}) }; }
361
359
  getColor(name) { return this.scope.getStore()?.colors[name] ?? this.colors[name]; }
362
- /** Classes d'un composant coloré (`getComponentClasses`). Seul le bouton plein est porté pour l'instant. */
360
+ /** Color classes of a colored component. */
363
361
  getComponentClasses(component, color) {
364
362
  if (!color)
365
363
  return [];
@@ -368,7 +366,7 @@ export class ColorManager {
368
366
  if (hit)
369
367
  return hit;
370
368
  let classes = [];
371
- // Seul le bouton a le gris pour couleur par défaut sans classe (HasDefaultGrayColor).
369
+ // Gray is the button's default color and gets no color classes; other components still get them.
372
370
  if (color !== 'gray' || component !== 'button') {
373
371
  classes = ['fi-color', 'fi-color-' + color.replace(/[^a-zA-Z0-9_-]/g, '')];
374
372
  const palette = this.getColor(color);
@@ -381,47 +379,47 @@ export class ColorManager {
381
379
  this.cache.set(key, classes);
382
380
  return classes;
383
381
  }
384
- /** Bloc `<style>` des variables `--<couleur>-<nuance>` injecté dans `<head>`. */
382
+ /** `<style>` block defining the `--<color>-<shade>` variables, injected into `<head>`. */
385
383
  renderCssVariables() {
386
384
  const vars = Object.entries(this.getColors()).map(([name, palette]) => Object.entries(palette).map(([shade, value]) => ` --${name}-${shade}:${value}; `).join('')).join('');
387
385
  return `<style>\n :root {\n ${vars}}\n</style>`;
388
386
  }
389
387
  }
390
- // ------------------------------------------------------------------ icônes (generate_icon_html + Heroicons)
388
+ // ------------------------------------------------------------------ icons
391
389
  const iconCache = new Map();
392
- /** Icône Heroicons (`m-eye`, `o-user`, `s-…`, `c-…`) avec les classes `fi-icon fi-size-<taille>`. */
390
+ /** Heroicons icon (`m-eye`, `o-user`, `s-…`, `c-…`) with the `fi-icon fi-size-<size>` classes. */
393
391
  export function icon(name, options = {}) {
394
392
  let svg = iconCache.get(name);
395
393
  if (svg === undefined) {
396
394
  const file = here(`../icons/heroicons/${name.replace(/^heroicon-/, '')}.svg`);
397
395
  if (!existsSync(file))
398
- throw new Error(`Icône introuvable : ${name}`);
396
+ throw new Error(`Icon not found: ${name}`);
399
397
  svg = readFileSync(file, 'utf8').trim();
400
398
  iconCache.set(name, svg);
401
399
  }
402
400
  const a = (options.attributes ?? new Attributes()).class(['fi-icon', `fi-size-${options.size ?? 'md'}`]);
403
401
  return svg.replace(/^<svg/, `<svg ${a.toHtml()}`);
404
402
  }
405
- /** Déclare une icône hors Heroicons (logo de marque, pictogramme maison) : un `<svg>` complet, fourni par l'application et non par l'utilisateur. */
403
+ /** Registers a non-Heroicons icon (brand logo, custom glyph): a full `<svg>` supplied by the application, never by end users (it is not sanitized). */
406
404
  export function registerIcon(name, svg) {
407
405
  if (!/^<svg[\s>]/.test(svg.trim()))
408
- throw new Error(`registerIcon(${name}) attend un élément <svg>.`);
406
+ throw new Error(`registerIcon(${name}) expects an <svg> element.`);
409
407
  iconCache.set(name, svg.trim());
410
408
  }
411
- /** Indicateur de chargement (`generate_loading_indicator_html`). */
409
+ /** Loading spinner SVG. */
412
410
  export function loadingIndicator(attributes = new Attributes(), size = 'md') {
413
411
  const a = attributes.class(['fi-icon fi-loading-indicator', `fi-size-${size}`]);
414
412
  return `<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" ${a.toHtml()}><path clip-rule="evenodd" d="M12 19C15.866 19 19 15.866 19 12C19 8.13401 15.866 5 12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19ZM12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" fill-rule="evenodd" fill="currentColor" opacity="0.2"></path><path d="M2 12C2 6.47715 6.47715 2 12 2V5C8.13401 5 5 8.13401 5 12H2Z" fill="currentColor"></path></svg>`;
415
413
  }
416
- // ------------------------------------------------------------------ vues EJS à espaces de noms, surchargeables
417
- /** Registre de vues : `filament-forms::components.text-input` → fichier EJS ; les dossiers de surcharge passent en premier. */
414
+ // ------------------------------------------------------------------ namespaced, overridable EJS views
415
+ /** View registry: `filament-forms::components.text-input` resolves to an EJS file; override directories are checked first. */
418
416
  export class ViewRegistry {
419
417
  namespaces = new Map();
420
418
  overrides = [];
421
419
  compiled = new Map();
422
420
  shared = {};
423
421
  namespace(name, dir) { this.namespaces.set(name, dir); return this; }
424
- /** Dossier de surcharge de l'application : `<dir>/<espace>/<chemin>.ejs` remplace la vue du paquet. */
422
+ /** Application override directory: `<dir>/<namespace>/<path>.ejs` replaces the package view. */
425
423
  overrideFrom(dir) { this.overrides.unshift(dir); this.compiled.clear(); return this; }
426
424
  share(data) { Object.assign(this.shared, data); return this; }
427
425
  resolve(view) {
@@ -434,7 +432,7 @@ export class ViewRegistry {
434
432
  }
435
433
  const base = this.namespaces.get(ns);
436
434
  if (!base)
437
- throw new Error(`Espace de noms de vues inconnu : ${ns}`);
435
+ throw new Error(`Unknown view namespace: ${ns}`);
438
436
  return path.join(base, rel);
439
437
  }
440
438
  render(view, data = {}) {
@@ -448,13 +446,13 @@ export class ViewRegistry {
448
446
  }
449
447
  }
450
448
  export const views = new ViewRegistry();
451
- // ------------------------------------------------------------------ réglages globaux (équivalent de configureUsing)
449
+ // ------------------------------------------------------------------ global configuration
452
450
  const configurators = new Map();
453
- /** Enregistre un réglage appliqué à chaque instance de la classe (et de ses sous-classes) créée ensuite par `make()`. */
451
+ /** Registers a callback applied to every instance of the class (and its subclasses) created afterwards by `make()`. */
454
452
  export function configureUsing(cls, configure) {
455
453
  configurators.set(cls, [...(configurators.get(cls) ?? []), configure]);
456
454
  }
457
- /** Applique les réglages globaux à une instance fraîchement créée, de la classe la plus générale à la plus précise. */
455
+ /** Applies the global callbacks to a new instance, from the most general class to the most specific. */
458
456
  export function configured(instance) {
459
457
  if (!configurators.size)
460
458
  return instance;
@@ -466,10 +464,10 @@ export function configured(instance) {
466
464
  configure(instance);
467
465
  return instance;
468
466
  }
469
- /** Oublie tous les réglages globaux (tests). */
467
+ /** Forgets all global callbacks (for tests). */
470
468
  export function resetConfigurators() { configurators.clear(); }
471
469
  const UNSCOPED = Symbol('unscoped');
472
- /** Registre de render hooks. Un hook sans portée est rendu partout ; avec portées, seulement quand l'appel porte l'une d'elles. */
470
+ /** Render hook registry. Unscoped hooks render everywhere; scoped hooks only when the call carries one of their scopes. */
473
471
  export class RenderHookRegistry {
474
472
  hooks = new Map();
475
473
  register(name, hook, scopes = null) {
@@ -480,7 +478,7 @@ export class RenderHookRegistry {
480
478
  byScope.set(scope, [...(byScope.get(scope) ?? []), hook]);
481
479
  return this;
482
480
  }
483
- /** Hooks à rendre pour cet appel, dans l'ordre de Filament : sans portée d'abord, puis portée par portée, chacun une seule fois. */
481
+ /** Hooks to render for a call: unscoped first, then scope by scope, each hook once. */
484
482
  collect(name, scopes) {
485
483
  const byScope = this.hooks.get(name);
486
484
  if (!byScope)
@@ -500,13 +498,13 @@ export class RenderHookRegistry {
500
498
  clear() { this.hooks.clear(); }
501
499
  }
502
500
  const toScopes = (scopes) => (scopes === null || scopes === undefined ? [] : Array.isArray(scopes) ? scopes : [scopes]);
503
- /** Hooks communs à tout le processus (tous les panels). */
501
+ /** Process-wide hooks (all panels). */
504
502
  export const renderHooks = new RenderHookRegistry();
505
503
  const renderHookScope = new AsyncLocalStorage();
506
- /** Exécute un rendu avec, en plus des hooks communs, ceux d'un registre propre (celui du panel de la requête). */
504
+ /** Renders with an extra registry (the request panel's) on top of the process-wide hooks. */
507
505
  export function runWithRenderHooks(registry, fn) { return renderHookScope.run(registry, fn); }
508
506
  const activeRegistries = () => { const own = renderHookScope.getStore(); return own && own !== renderHooks ? [renderHooks, own] : [renderHooks]; };
509
- /** Rend un emplacement : hooks communs, puis hooks du panel en cours. */
507
+ /** Renders a slot: process-wide hooks, then the current panel's hooks. */
510
508
  export function renderHook(name, scopes = null, data = {}) {
511
509
  return activeRegistries().map((registry) => registry.render(name, scopes, data)).join('');
512
510
  }
package/lib/ui.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { cls, e, js, loadingIndicator, type ColorManager } from './index.ts';
2
- /** Gestionnaire de couleurs utilisé par les composants (celui du panel, ou un gestionnaire par défaut). */
2
+ /** Sets the color manager used by the components (usually the panel's). */
3
3
  export declare const useColorManager: (colors: ColorManager) => void;
4
4
  export type Attrs = Record<string, string | number | boolean | null | undefined>;
5
5
  type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
@@ -11,7 +11,7 @@ export type BadgeProps = {
11
11
  tag?: string;
12
12
  attributes?: Attrs;
13
13
  };
14
- /** Pastille (`<x-filament::badge>`). */
14
+ /** Badge. */
15
15
  export declare function badge(props?: BadgeProps, slot?: string): string;
16
16
  export type IconButtonProps = {
17
17
  icon: string;
@@ -25,7 +25,7 @@ export type IconButtonProps = {
25
25
  badgeColor?: string;
26
26
  attributes?: Attrs;
27
27
  };
28
- /** Bouton réduit à une icône (`<x-filament::icon-button>`) ; le libellé sert d'info-bulle et de nom accessible. */
28
+ /** Icon-only button; the label is used as tooltip and accessible name. */
29
29
  export declare function iconButton(props: IconButtonProps): string;
30
30
  export type ButtonProps = {
31
31
  color?: string;
@@ -41,7 +41,7 @@ export type ButtonProps = {
41
41
  badgeColor?: string;
42
42
  attributes?: Attrs;
43
43
  };
44
- /** Bouton (`<x-filament::button>`). */
44
+ /** Button. */
45
45
  export declare function button(props?: ButtonProps, slot?: string): string;
46
46
  export type LinkProps = {
47
47
  href?: string | null;
@@ -55,9 +55,9 @@ export type LinkProps = {
55
55
  badgeColor?: string;
56
56
  attributes?: Attrs;
57
57
  };
58
- /** Lien (`<x-filament::link>`). */
58
+ /** Link. */
59
59
  export declare function link(props?: LinkProps, slot?: string): string;
60
- /** Avatar (`<x-filament::avatar>`) : rond par défaut. */
60
+ /** Avatar image, circular by default. */
61
61
  export declare function avatar(props: {
62
62
  src: string;
63
63
  alt?: string;
@@ -66,20 +66,20 @@ export declare function avatar(props: {
66
66
  attributes?: Attrs;
67
67
  }): string;
68
68
  /**
69
- * Fil d'Ariane (`<x-filament::breadcrumbs>`). Comme dans Filament : un élément sans `url` est un simple texte ;
70
- * un élément dont l'`url` est vide reste un lien, mais sans adresse. Le dernier élément est la page courante.
69
+ * Breadcrumbs. An item without `url` is plain text; an item with an empty `url` is still a link, without `href`.
70
+ * The last item is the current page.
71
71
  */
72
72
  export declare function breadcrumbs(items: {
73
73
  label: string;
74
74
  url?: string | null;
75
75
  }[], label?: string): string;
76
- /** Case à cocher nue (`<x-filament::input.checkbox>`). */
76
+ /** Bare checkbox. */
77
77
  export declare function checkbox(props?: {
78
78
  checked?: boolean;
79
79
  valid?: boolean;
80
80
  attributes?: Attrs;
81
81
  }): string;
82
- /** Roue d'attente (`<x-filament::loading-indicator>`). */
82
+ /** Loading spinner. */
83
83
  export declare function spinner(attributes?: Attrs): string;
84
84
  export type InputWrapperProps = {
85
85
  prefix?: string | null;
@@ -94,13 +94,13 @@ export type InputWrapperProps = {
94
94
  inlineSuffix?: boolean;
95
95
  attributes?: Attrs;
96
96
  };
97
- /** Habillage d'un champ (`<x-filament::input.wrapper>`) : bordure, préfixe et suffixe (texte ou icône). */
97
+ /** Input wrapper: border, prefix and suffix (text or icon). */
98
98
  export declare function inputWrapper(props?: InputWrapperProps, slot?: string): string;
99
- /** Champ texte nu (`<x-filament::input>`), à placer dans `inputWrapper`. */
99
+ /** Bare text input, meant to be placed in `inputWrapper`. */
100
100
  export declare function input(attributes?: Attrs): string;
101
- /** Liste déroulante native nue (`<x-filament::input.select>`) ; `slot` contient les `<option>`. */
101
+ /** Bare native select; `slot` holds the `<option>` elements. */
102
102
  export declare function select(attributes?: Attrs, slot?: string): string;
103
- /** Groupe de champs encadré (`<x-filament::fieldset>`). */
103
+ /** Fieldset with an optional legend. */
104
104
  export declare function fieldset(props?: {
105
105
  label?: string | null;
106
106
  labelHidden?: boolean;
@@ -117,7 +117,7 @@ export type CalloutProps = {
117
117
  controls?: string | null;
118
118
  attributes?: Attrs;
119
119
  };
120
- /** Encart d'information (`<x-filament::callout>`). `footer` et `controls` reçoivent du HTML déjà rendu (boutons…). */
120
+ /** Callout box. `footer` and `controls` take already rendered HTML (buttons…). */
121
121
  export declare function callout(props?: CalloutProps): string;
122
122
  export type EmptyStateProps = {
123
123
  heading: string;
@@ -129,21 +129,21 @@ export type EmptyStateProps = {
129
129
  contained?: boolean;
130
130
  attributes?: Attrs;
131
131
  };
132
- /** État vide (`<x-filament::empty-state>`) : icône, titre, description, pied (actions déjà rendues). */
132
+ /** Empty state: icon, heading, description and footer (already rendered actions). */
133
133
  export declare function emptyState(props: EmptyStateProps): string;
134
134
  export type PaginationProps = {
135
- /** Identifiant du composant hôte (préfixe des clés `wire:key`). */
135
+ /** Host component id (prefix of the `wire:key` values). */
136
136
  id: string;
137
137
  page: number;
138
138
  perPage: number;
139
- /** Nombre total d'enregistrements ; absent pour une pagination simple (précédent / suivant). */
139
+ /** Total record count; omit it for simple (previous / next) pagination. */
140
140
  total?: number;
141
141
  hasMore?: boolean;
142
142
  pageOptions?: (number | 'all')[];
143
143
  extremeLinks?: boolean;
144
144
  property?: string;
145
145
  };
146
- /** Pagination (`<x-filament::pagination>`), liée aux méthodes `previousPage`, `nextPage` et `gotoPage` du composant hôte. */
146
+ /** Pagination bound to the host component's `previousPage`, `nextPage` and `gotoPage` methods. */
147
147
  export declare function pagination(props: PaginationProps): string;
148
148
  export type SectionProps = {
149
149
  heading?: string | null;
@@ -164,7 +164,7 @@ export type SectionProps = {
164
164
  secondary?: boolean;
165
165
  attributes?: Attrs;
166
166
  };
167
- /** Section (`<x-filament::section>`) : carte avec en-tête facultatif (icône, titre, description, zone d'actions), repliable. */
167
+ /** Collapsible card with an optional header (icon, heading, description, after-header area). */
168
168
  export declare function section(props?: SectionProps, slot?: string): string;
169
169
  export type TabItem = {
170
170
  label: string;
@@ -177,7 +177,7 @@ export type TabItem = {
177
177
  href?: string | null;
178
178
  attributes?: Attrs;
179
179
  };
180
- /** Onglets (`<x-filament::tabs>` et `<x-filament::tabs.item>`). */
180
+ /** Tabs. */
181
181
  export declare function tabs(props: {
182
182
  label?: string | null;
183
183
  vertical?: boolean;
@@ -197,11 +197,11 @@ export type DropdownItem = {
197
197
  disabled?: boolean;
198
198
  attributes?: Attrs;
199
199
  };
200
- /** Élément de menu (`<x-filament::dropdown.list.item>`). */
200
+ /** Dropdown menu item. */
201
201
  export declare function dropdownItem(item: DropdownItem): string;
202
- /** Liste d'éléments de menu (`<x-filament::dropdown.list>`). */
202
+ /** List of dropdown menu items. */
203
203
  export declare function dropdownList(items: DropdownItem[]): string;
204
- /** Menu déroulant (`<x-filament::dropdown>`) : `trigger` est le HTML du déclencheur, `slot` le contenu du panneau. */
204
+ /** Dropdown: `trigger` is the trigger's HTML, `slot` the panel content. */
205
205
  export declare function dropdown(props: {
206
206
  trigger: string;
207
207
  width?: string | null;
@@ -217,7 +217,7 @@ export type ModalProps = {
217
217
  eyebrow?: string | null;
218
218
  icon?: string | null;
219
219
  iconColor?: string;
220
- /** Boutons du pied (HTML déjà rendu), ou `footer` pour un pied entièrement libre. */
220
+ /** Footer buttons (already rendered HTML), or `footer` for a fully custom footer. */
221
221
  footerActions?: string[];
222
222
  footer?: string | null;
223
223
  footerActionsAlignment?: 'start' | 'center' | 'end';
@@ -230,10 +230,10 @@ export type ModalProps = {
230
230
  closeButton?: boolean;
231
231
  closeByClickingAway?: boolean;
232
232
  closeByEscaping?: boolean;
233
- /** Identifiant du composant hôte, préfixe des clés `wire:key` quand la modale vit dans un composant à état. */
233
+ /** Host component id, used to prefix `wire:key` values when the modal lives in a stateful component. */
234
234
  livewireId?: string | null;
235
235
  };
236
- /** Fenêtre modale (`<x-filament::modal>`), ouverte par l'événement `open-modal` portant son identifiant. */
236
+ /** Modal, opened by an `open-modal` event carrying its id. */
237
237
  export declare function modal(props: ModalProps, slot?: string): string;
238
238
  export declare const ui: {
239
239
  avatar: typeof avatar;
package/lib/ui.js CHANGED
@@ -1,35 +1,32 @@
1
- // @navette-js/support/ui — les composants d'interface de base de Filament, utilisables seuls (équivalent des composants Blade
2
- // `<x-filament::button>`, `<x-filament::badge>`…). Chaque fonction rend le même balisage que la vue Blade d'origine
3
- // (sources/filament/packages/support/resources/views/components) ; elles sont vérifiées scène par scène contre l'application
4
- // de documentation de Filament (`node tools/parity/run-scenes.mjs`).
1
+ // @navette-js/support/ui — base UI components (button, badge, modal…) usable on their own, outside a panel.
2
+ // Each function returns an HTML string.
5
3
  import { Attributes, cls, e, icon, js, loadingIndicator, views } from "./index.js";
6
4
  import { t, tc } from "./i18n.js";
7
5
  import { fileURLToPath } from 'node:url';
8
- // Les vues de base (`filament::components.*`) vivent dans ce paquet : elles sont utilisables sans panel.
6
+ // The base views (`filament::components.*`) live in this package so they work without a panel.
9
7
  views.namespace('filament', fileURLToPath(new URL('../views', import.meta.url)));
10
8
  let manager = null;
11
- /** Gestionnaire de couleurs utilisé par les composants (celui du panel, ou un gestionnaire par défaut). */
9
+ /** Sets the color manager used by the components (usually the panel's). */
12
10
  export const useColorManager = (colors) => { manager = colors; };
13
11
  const colorsOf = (component, color) => {
14
12
  if (!manager)
15
- throw new Error('Aucun gestionnaire de couleurs : appelez useColorManager().');
13
+ throw new Error('No color manager: call useColorManager() first.');
16
14
  return color && color !== 'gray' ? manager.getComponentClasses(component, color) : [];
17
15
  };
18
- /** Pastille (`<x-filament::badge>`). */
16
+ /** Badge. */
19
17
  export function badge(props = {}, slot = '') {
20
18
  const { color = 'primary', size = 'md', tag = 'span' } = props;
21
19
  const attributes = new Attributes(props.attributes ?? {}).class([...colorsOf('badge', color), 'fi-badge', `fi-size-${size}`]);
22
20
  const iconHtml = props.icon ? icon(props.icon, { size: 'sm' }) : '';
23
21
  return `<${tag} ${attributes.toHtml()}>${props.iconPosition === 'after' ? '' : iconHtml}<span class="fi-badge-label-ctn"><span class="fi-badge-label">${slot}</span></span>${props.iconPosition === 'after' ? iconHtml : ''}</${tag}>`;
24
22
  }
25
- /** Petit compteur posé sur un bouton ou un lien (rendu sans enveloppe de libellé, comme dans Filament). */
23
+ /** Small counter on a button or link (rendered without the badge label wrapper). */
26
24
  const counter = (value, color = 'primary') => `<span class="${cls([...colorsOf('badge', color), 'fi-badge', 'fi-size-xs'])}">${e(String(value))}</span>`;
27
- /** Taille de l'icône selon celle du bouton (comme la vue Blade). */
28
25
  const ICON_BUTTON_ICON_SIZE = { xs: 'sm', sm: 'md', md: 'md', lg: 'lg', xl: 'lg' };
29
- /** Bouton réduit à une icône (`<x-filament::icon-button>`) ; le libellé sert d'info-bulle et de nom accessible. */
26
+ /** Icon-only button; the label is used as tooltip and accessible name. */
30
27
  export function iconButton(props) {
31
28
  const { color = 'primary', size = 'md', disabled = false, tag = 'button' } = props;
32
- // Les classes de l'appelant viennent après celles du composant (comme `$attributes->class()` de Laravel).
29
+ // The caller's classes go after the component's own classes.
33
30
  const { class: extraClass = null, ...rest } = (props.attributes ?? {});
34
31
  const attributes = new Attributes(rest)
35
32
  .merge({
@@ -40,7 +37,7 @@ export function iconButton(props) {
40
37
  const badgeHtml = props.badge !== null && props.badge !== undefined ? `<div class="fi-icon-btn-badge-ctn">${counter(props.badge, props.badgeColor)}</div>` : '';
41
38
  return `<${tag} ${attributes.toHtml()}>${icon(props.icon, { size: ICON_BUTTON_ICON_SIZE[size] })}${badgeHtml}</${tag}>`;
42
39
  }
43
- /** Bouton (`<x-filament::button>`). */
40
+ /** Button. */
44
41
  export function button(props = {}, slot = '') {
45
42
  const { color = 'primary', size = 'md', outlined = false, disabled = false, tag = 'button', type = 'button', iconPosition = 'before' } = props;
46
43
  const attributes = new Attributes(props.attributes ?? {})
@@ -53,7 +50,7 @@ export function button(props = {}, slot = '') {
53
50
  const badgeHtml = props.badge !== null && props.badge !== undefined ? `<div class="fi-btn-badge-ctn">${counter(props.badge, props.badgeColor)}</div>` : '';
54
51
  return `<${tag} ${attributes.toHtml()}>${iconPosition === 'before' ? iconHtml : ''}${slot}${iconPosition === 'after' ? iconHtml : ''}${badgeHtml}</${tag}>`;
55
52
  }
56
- /** Lien (`<x-filament::link>`). */
53
+ /** Link. */
57
54
  export function link(props = {}, slot = '') {
58
55
  const { color = 'primary', size = 'md', tag = 'a', iconPosition = 'before' } = props;
59
56
  const attributes = new Attributes(props.attributes ?? {})
@@ -64,37 +61,37 @@ export function link(props = {}, slot = '') {
64
61
  return `<${tag} ${attributes.toHtml()}>${iconPosition === 'before' ? iconHtml : ''}<span class="fi-link-label">${slot}</span>${iconPosition === 'after' ? iconHtml : ''}${badgeHtml}</${tag}>`;
65
62
  }
66
63
  // ------------------------------------------------------------------ avatar
67
- /** Avatar (`<x-filament::avatar>`) : rond par défaut. */
64
+ /** Avatar image, circular by default. */
68
65
  export function avatar(props) {
69
66
  const attributes = new Attributes(props.attributes ?? {}).merge({ alt: props.alt ?? null, src: props.src }).class(['fi-avatar', { 'fi-circular': props.circular !== false }, `fi-size-${props.size ?? 'md'}`]);
70
67
  return `<img ${attributes.toHtml()} />`;
71
68
  }
72
69
  // ------------------------------------------------------------------ breadcrumbs
73
70
  /**
74
- * Fil d'Ariane (`<x-filament::breadcrumbs>`). Comme dans Filament : un élément sans `url` est un simple texte ;
75
- * un élément dont l'`url` est vide reste un lien, mais sans adresse. Le dernier élément est la page courante.
71
+ * Breadcrumbs. An item without `url` is plain text; an item with an empty `url` is still a link, without `href`.
72
+ * The last item is the current page.
76
73
  */
77
74
  export function breadcrumbs(items, label = 'Breadcrumb') {
78
75
  const separators = icon('m-chevron-right', { attributes: new Attributes({ 'aria-hidden': 'true', class: 'fi-breadcrumbs-item-separator fi-ltr' }) }) + icon('m-chevron-left', { attributes: new Attributes({ 'aria-hidden': 'true', class: 'fi-breadcrumbs-item-separator fi-rtl' }) });
79
76
  const list = items.map((item, index) => `<li class="fi-breadcrumbs-item">${index > 0 ? separators : ''}${((current) => (item.url === undefined || item.url === null ? `<span${current} class="fi-breadcrumbs-item-label">${e(item.label)}</span>` : `<a${item.url ? ` href="${e(item.url)}"` : ''}${current} class="fi-breadcrumbs-item-label">${e(item.label)}</a>`))(index === items.length - 1 ? ' aria-current="page"' : '')}</li>`);
80
77
  return `<nav class="fi-breadcrumbs" aria-label="${e(label)}"><ol class="fi-breadcrumbs-list">${list.join('')}</ol></nav>`;
81
78
  }
82
- // ------------------------------------------------------------------ champs de base
83
- /** Case à cocher nue (`<x-filament::input.checkbox>`). */
79
+ // ------------------------------------------------------------------ basic inputs
80
+ /** Bare checkbox. */
84
81
  export function checkbox(props = {}) {
85
82
  const attributes = new Attributes(props.attributes ?? {}).merge({ type: 'checkbox', checked: props.checked === true }).class(['fi-checkbox-input', props.valid === false ? 'fi-invalid' : 'fi-valid']);
86
83
  return `<input ${attributes.toHtml()} />`;
87
84
  }
88
- /** Roue d'attente (`<x-filament::loading-indicator>`). */
85
+ /** Loading spinner. */
89
86
  export function spinner(attributes = {}) {
90
- // Les classes de l'appelant viennent après celles du composant.
87
+ // The caller's classes go after the component's own classes.
91
88
  const { class: extra, ...rest } = attributes;
92
89
  const html = loadingIndicator(new Attributes(rest));
93
90
  return extra ? html.replace(/class="([^"]*)"/, (_, own) => `class="${own} ${e(String(extra))}"`) : html;
94
91
  }
95
92
  const FOCUS_INPUT = "if (! $event.target.closest('.fi-input-wrp-actions')) $dispatch('focus-input')";
96
93
  const affixIcon = (name, color) => icon(name, { attributes: new Attributes().class(color && color !== 'gray' ? ['fi-color', `fi-color-${color}`] : []) });
97
- /** Habillage d'un champ (`<x-filament::input.wrapper>`) : bordure, préfixe et suffixe (texte ou icône). */
94
+ /** Input wrapper: border, prefix and suffix (text or icon). */
98
95
  export function inputWrapper(props = {}, slot = '') {
99
96
  const hasPrefix = !!(props.prefix || props.prefixIcon), hasSuffix = !!(props.suffix || props.suffixIcon);
100
97
  const attributes = new Attributes(props.attributes ?? {}).class(['fi-input-wrp', { 'fi-disabled': props.disabled === true, 'fi-invalid': props.valid === false }]);
@@ -102,31 +99,31 @@ export function inputWrapper(props = {}, slot = '') {
102
99
  const suffix = hasSuffix ? `<div x-on:click="${FOCUS_INPUT}" class="${cls(['fi-input-wrp-suffix', { 'fi-inline': props.inlineSuffix === true, 'fi-input-wrp-suffix-has-label': !!props.suffix }])}">${props.suffix ? `<span class="fi-input-wrp-label">${e(props.suffix)}</span>` : ''}${props.suffixIcon ? affixIcon(props.suffixIcon, props.suffixIconColor) : ''}</div>` : '';
103
100
  return `<div ${attributes.toHtml()}>${prefix}<div class="fi-input-wrp-content-ctn">${slot}</div>${suffix}</div>`;
104
101
  }
105
- /** Champ texte nu (`<x-filament::input>`), à placer dans `inputWrapper`. */
102
+ /** Bare text input, meant to be placed in `inputWrapper`. */
106
103
  export function input(attributes = {}) { return `<input ${new Attributes(attributes).class(['fi-input']).toHtml()} />`; }
107
- /** Liste déroulante native nue (`<x-filament::input.select>`) ; `slot` contient les `<option>`. */
104
+ /** Bare native select; `slot` holds the `<option>` elements. */
108
105
  export function select(attributes = {}, slot = '') { return `<select ${new Attributes(attributes).class(['fi-select-input']).toHtml()}>${slot}</select>`; }
109
- /** Groupe de champs encadré (`<x-filament::fieldset>`). */
106
+ /** Fieldset with an optional legend. */
110
107
  export function fieldset(props = {}, slot = '') {
111
108
  const attributes = new Attributes(props.attributes ?? {}).class(['fi-fieldset', { 'fi-fieldset-label-hidden': props.labelHidden === true }]);
112
109
  return `<fieldset ${attributes.toHtml()}>${props.label ? `<legend>${e(props.label)}</legend>` : ''}${slot}</fieldset>`;
113
110
  }
114
111
  const colorMark = (color) => (color && color !== 'gray' ? ['fi-color', `fi-color-${color}`] : []);
115
- /** Encart d'information (`<x-filament::callout>`). `footer` et `controls` reçoivent du HTML déjà rendu (boutons…). */
112
+ /** Callout box. `footer` and `controls` take already rendered HTML (buttons…). */
116
113
  export function callout(props = {}) {
117
114
  const attributes = new Attributes(props.attributes ?? {}).class(['fi-callout', ...colorMark(props.color)]);
118
115
  const iconHtml = props.icon ? icon(props.icon, { size: props.iconSize ?? 'lg', attributes: new Attributes().class([...colorMark(props.iconColor ?? props.color), 'fi-callout-icon']) }) : '';
119
116
  const text = `<div class="fi-callout-text">${props.heading ? `<h4 class="fi-callout-heading">${e(props.heading)}</h4>` : ''}${props.description ? `<p class="fi-callout-description">${e(props.description)}</p>` : ''}</div>`;
120
117
  return `<div ${attributes.toHtml()}>${iconHtml}<div class="fi-callout-main">${text}${props.footer ? `<div class="fi-callout-footer">${props.footer}</div>` : ''}</div>${props.controls ? `<div class="fi-callout-controls">${props.controls}</div>` : ''}</div>`;
121
118
  }
122
- /** État vide (`<x-filament::empty-state>`) : icône, titre, description, pied (actions déjà rendues). */
119
+ /** Empty state: icon, heading, description and footer (already rendered actions). */
123
120
  export function emptyState(props) {
124
121
  const color = props.iconColor ?? 'primary';
125
122
  const attributes = new Attributes(props.attributes ?? {}).class(['fi-empty-state', { 'fi-empty-state-not-contained': props.contained === false }]);
126
123
  const iconHtml = props.icon ? `<div class="${cls(['fi-empty-state-icon-bg', ...colorMark(color)])}">${icon(props.icon, { size: props.iconSize ?? 'lg', attributes: new Attributes().class(colorMark(color)) })}</div>` : '';
127
124
  return `<div ${attributes.toHtml()}><div class="fi-empty-state-content">${iconHtml}<div class="fi-empty-state-text-ctn"><h2 class="fi-empty-state-heading">${e(props.heading)}</h2>${props.description ? `<p class="fi-empty-state-description">${e(props.description)}</p>` : ''}${props.footer ? `<footer class="fi-empty-state-footer">${props.footer}</footer>` : ''}</div></div></div>`;
128
125
  }
129
- /** Numéros de page affichés : tous jusqu'à onze pages, sinon une fenêtre autour de la page courante. */
126
+ /** Page numbers to show: all of them up to eleven pages, otherwise a window around the current page. */
130
127
  function pageElements(current, last) {
131
128
  if (last <= 11)
132
129
  return Array.from({ length: last }, (_, i) => i + 1);
@@ -139,7 +136,7 @@ function pageElements(current, last) {
139
136
  }
140
137
  return out;
141
138
  }
142
- /** Pagination (`<x-filament::pagination>`), liée aux méthodes `previousPage`, `nextPage` et `gotoPage` du composant hôte. */
139
+ /** Pagination bound to the host component's `previousPage`, `nextPage` and `gotoPage` methods. */
143
140
  export function pagination(props) {
144
141
  const simple = props.total === undefined;
145
142
  const lastPage = simple ? props.page + (props.hasMore ? 1 : 0) : Math.max(1, Math.ceil(props.total / props.perPage));
@@ -153,7 +150,7 @@ export function pagination(props) {
153
150
  attrs: (o) => new Attributes(o),
154
151
  });
155
152
  }
156
- /** Section (`<x-filament::section>`) : carte avec en-tête facultatif (icône, titre, description, zone d'actions), repliable. */
153
+ /** Collapsible card with an optional header (icon, heading, description, after-header area). */
157
154
  export function section(props = {}, slot = '') {
158
155
  const collapsible = props.collapsible === true, collapsed = collapsible && props.collapsed === true;
159
156
  const hasText = !!props.heading || !!props.description;
@@ -187,7 +184,7 @@ export function section(props = {}, slot = '') {
187
184
  const body = slot !== '' || props.footer ? `<div${collapsible ? ` x-bind:id="$id('fi-section-content')"${collapsed ? ' x-cloak' : ''}` : ''} class="fi-section-content-ctn">${slot !== '' ? `<div class="fi-section-content">${slot}</div>` : ''}${props.footer ? `<footer class="fi-section-footer">${props.footer}</footer>` : ''}</div>` : '';
188
185
  return `<section ${attributes.toHtml()}>${header}${body}</section>`;
189
186
  }
190
- /** Onglets (`<x-filament::tabs>` et `<x-filament::tabs.item>`). */
187
+ /** Tabs. */
191
188
  export function tabs(props, items) {
192
189
  const attributes = new Attributes(props.attributes ?? {}).merge({ 'aria-label': props.label ?? null }).class(['fi-tabs', { 'fi-contained': props.contained === true, 'fi-vertical': props.vertical === true }]);
193
190
  const html = items.map((item) => {
@@ -201,7 +198,7 @@ export function tabs(props, items) {
201
198
  });
202
199
  return `<nav ${attributes.toHtml()}>${html.join('')}</nav>`;
203
200
  }
204
- /** Élément de menu (`<x-filament::dropdown.list.item>`). */
201
+ /** Dropdown menu item. */
205
202
  export function dropdownItem(item) {
206
203
  const tag = item.tag ?? 'button';
207
204
  const color = item.color ?? 'gray';
@@ -216,15 +213,15 @@ export function dropdownItem(item) {
216
213
  const badgeHtml = item.badge !== null && item.badge !== undefined ? `<span class="${cls([...colorsOf('badge', item.badgeColor ?? 'primary'), 'fi-badge'])}">${e(String(item.badge))}</span>` : '';
217
214
  return `<${tag} ${attributes.toHtml()}>${lead}<span class="fi-dropdown-list-item-label">${e(item.label)}</span>${badgeHtml}</${tag}>`;
218
215
  }
219
- /** Liste d'éléments de menu (`<x-filament::dropdown.list>`). */
216
+ /** List of dropdown menu items. */
220
217
  export function dropdownList(items) { return `<div class="fi-dropdown-list">${items.map(dropdownItem).join('')}</div>`; }
221
- /** Menu déroulant (`<x-filament::dropdown>`) : `trigger` est le HTML du déclencheur, `slot` le contenu du panneau. */
218
+ /** Dropdown: `trigger` is the trigger's HTML, `slot` the panel content. */
222
219
  export function dropdown(props, slot = '') {
223
220
  const attributes = new Attributes(props.attributes ?? {}).merge({ 'x-data': 'filamentDropdown({ isMenu: true })' }).class(['fi-dropdown']);
224
221
  const float = `x-float${props.placement ? `.placement.${props.placement}` : ''}.flip.offset`;
225
222
  return `<div ${attributes.toHtml()}><div x-on:mousedown="if ($event.button === 0) toggle($event)" class="fi-dropdown-trigger">${props.trigger}</div><div x-cloak ${float}="{ offset: ${props.offset ?? 8}, }" x-ref="panel" x-transition:enter-start="fi-opacity-0" x-transition:leave-end="fi-opacity-0" role="menu" class="${cls(['fi-dropdown-panel', props.width ? `fi-width-${props.width}` : null, { 'fi-scrollable': !!props.maxHeight }])}" style="${props.maxHeight ? `max-height: ${e(props.maxHeight)};` : ''}">${slot}</div></div>`;
226
223
  }
227
- /** Fenêtre modale (`<x-filament::modal>`), ouverte par l'événement `open-modal` portant son identifiant. */
224
+ /** Modal, opened by an `open-modal` event carrying its id. */
228
225
  export function modal(props, slot = '') {
229
226
  const { id, alignment = 'start', width = 'sm', closeButton = true, closeByClickingAway = true, closeByEscaping = true } = props;
230
227
  const hasHeading = !!props.heading, hasDescription = !!props.description && hasHeading;
@@ -233,7 +230,7 @@ export function modal(props, slot = '') {
233
230
  const key = (part) => `${props.livewireId ? `${props.livewireId}.` : ''}modal.${id}.${part}`;
234
231
  const close = `$dispatch('close-modal', { id: ${js(id)} })`;
235
232
  const iconColor = props.iconColor ?? 'primary';
236
- // Fenêtre défilante (panneau latéral, en-tête ou pied fixes) : le piège de focus est posé sur la fenêtre elle-même, pas sur son conteneur.
233
+ // Scrolling window (slide-over, sticky header or footer): the focus trap goes on the window itself, not its container.
237
234
  const trapOnWindow = props.slideOver === true || props.stickyHeader === true || props.stickyFooter === true;
238
235
  const closeHtml = closeButton ? iconButton({ color: 'gray', icon: 'o-x-mark', label: t('filament::components/modal.actions.close.label'), attributes: { tabindex: closeByEscaping ? '-1' : null, 'x-on:click': close, class: 'fi-modal-close-btn' } }).replace('fi-icon fi-size-md', 'fi-icon fi-size-lg') : '';
239
236
  const iconHtml = props.icon ? `<div class="fi-modal-icon-ctn"><div class="${cls([...colorMark(iconColor), 'fi-modal-icon-bg'])}">${icon(props.icon, { size: 'lg' })}</div></div>` : '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@navette-js/support",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.2",
4
4
  "description": "Shared utilities, colors, icons, views and translations for Navette.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,7 +42,6 @@
42
42
  },
43
43
  "keywords": [
44
44
  "navette",
45
- "filament",
46
45
  "admin",
47
46
  "panel",
48
47
  "livewire",
@@ -1,4 +1,4 @@
1
- <%# Badge — port de la vue Blade filament::components.badge (sans icône ni bouton de suppression) %>
1
+ <%# Badge without icon or delete button. %>
2
2
  <span class="<%- cls([...colorClasses, 'fi-badge', 'fi-size-' + (locals.size ?? 'md')]) %>">
3
3
  <span class="fi-badge-label-ctn">
4
4
  <span class="fi-badge-label">
@@ -1,5 +1,5 @@
1
- <%# Bouton icône — port de la vue Blade filament::components.icon-button (couleur grise par défaut, sans badge).
2
- `extra` : attributs placés après les attributs de chargement ; pour la modale, `tabindex` précède `type` et le clic vient en dernier. %>
1
+ <%# Gray icon button without badge.
2
+ `extra`: attributes placed after the loading attributes; with `after` (modal close button), `extra` goes before `type` and `after` comes last. %>
3
3
  <% const lead = locals.after ? (locals.extra ? extra.toHtml() + ' ' : '') : ''; const tail = locals.after ? after.toHtml() : (locals.extra ? extra.toHtml() : ''); %>
4
4
  <button class="fi-icon-btn fi-size-<%- locals.size ?? 'md' %> <%- className %>" title="<%= label %>" aria-label="<%= label %>" <%- lead %>type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled"<% if (tail) { %> <%- tail %><% } %>>
5
5
  <%- icon(iconName, { size: locals.iconSize ?? 'md' }) %>
@@ -1,5 +1,5 @@
1
- <%# Pagination — port de la vue Blade filament::components.pagination.
2
- Options : simple (précédent / suivant seulement), extremeLinks (premier / dernier), property (propriété liée au nombre par page). Sans curseur. %>
1
+ <%# Pagination. Options: simple (previous / next only), extremeLinks (first / last), property (property bound to the per-page count).
2
+ Cursor pagination is not supported. %>
3
3
  <% const simple = locals.simple === true, extremeLinks = locals.extremeLinks === true, property = locals.property ?? 'tableRecordsPerPage'; %>
4
4
  <% const perPageSelect = () => { %>
5
5
  <select class="fi-select-input" wire:model.live="<%= property %>">