@navette-js/actions 0.0.0-stage → 1.0.0-beta.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NavetteJS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,9 @@
1
- # Temporary Holding Version
1
+ # @navette-js/actions
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Actions and modals for Navette, the Node.js port of Filament.
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`.
6
+
7
+ **Beta:** the API may still change between beta releases.
8
+
9
+ License: MIT. Third-party notices: see `THIRD-PARTY-NOTICES.md` where present.
package/lib/index.d.ts ADDED
@@ -0,0 +1,183 @@
1
+ import { type ColorManager, type Lazy } from '@navette-js/support';
2
+ import type { ComponentContext, Renderable } from '@navette-js/schemas';
3
+ /** Emplacements de render hook d'une modale d'action — mêmes noms que `Filament\Actions\View\ActionsRenderHook`. Donnée reçue : `data.action`. */
4
+ export declare const ActionsRenderHook: {
5
+ readonly MODAL_CUSTOM_CONTENT_AFTER: "actions::modal.custom-content.after";
6
+ readonly MODAL_CUSTOM_CONTENT_BEFORE: "actions::modal.custom-content.before";
7
+ readonly MODAL_CUSTOM_CONTENT_FOOTER_AFTER: "actions::modal.custom-content-footer.after";
8
+ readonly MODAL_CUSTOM_CONTENT_FOOTER_BEFORE: "actions::modal.custom-content-footer.before";
9
+ readonly MODAL_SCHEMA_AFTER: "actions::modal.schema.after";
10
+ readonly MODAL_SCHEMA_BEFORE: "actions::modal.schema.before";
11
+ };
12
+ export declare const useColorManager: (manager: ColorManager) => void;
13
+ /** Contexte d'exécution d'une action montée. */
14
+ export type ActionCallContext = {
15
+ livewire: any;
16
+ record: any;
17
+ arguments: Record<string, unknown>;
18
+ data: Record<string, unknown>;
19
+ };
20
+ export declare class Action implements Renderable {
21
+ /** Réglage par défaut de toutes les instances créées ensuite par `make()` : `TextInput.configureUsing((input) => input.maxLength(255))`. */
22
+ static configureUsing<T>(this: abstract new (...args: any[]) => T, configure: (instance: T) => void): void;
23
+ private name;
24
+ private _label;
25
+ private _color;
26
+ private _size;
27
+ private _submit;
28
+ private _hidden;
29
+ private _visible;
30
+ private _disabled;
31
+ private _url;
32
+ private _action;
33
+ private _handler;
34
+ private _requiresConfirmation;
35
+ private _modalHeading;
36
+ private _modalDescription;
37
+ private _modalSubmitActionLabel;
38
+ private _modalIcon;
39
+ private _icon;
40
+ private _alpineClick;
41
+ private _keyBindings;
42
+ private _schema;
43
+ private _extraModalFooterActions;
44
+ private _schemaColumns;
45
+ private _fillForm;
46
+ private _modalWidth;
47
+ private _lazyUrl;
48
+ private _tableIcon;
49
+ private _groupedIcon;
50
+ private _lazyColor;
51
+ private _lazyIcon;
52
+ constructor(name: string);
53
+ static make(name: string): Action;
54
+ label(label: Lazy<string | null, ComponentContext>): this;
55
+ /** Une fonction rend la couleur dépendante de l'enregistrement (actions de ligne). */
56
+ color(color: string | ((ctx: ComponentContext) => string | Promise<string>)): this;
57
+ size(size: string): this;
58
+ /** Bouton `type="submit"` qui déclenche la méthode de soumission du formulaire (`->submit('authenticate')`). */
59
+ submit(form: string | null): this;
60
+ hidden(condition?: Lazy<boolean, ComponentContext>): this;
61
+ /** Action de navigation : rendue en `<a href wire:navigate>`. */
62
+ url(url: string | null | ((ctx: ComponentContext) => string | null)): this;
63
+ /** Une chaîne : méthode du composant hôte appelée au clic (`wire:click`). Une fonction : traitement de l'action, monté par `mountAction()` (avec modale si elle en a une). */
64
+ action(action: string | ((ctx: ActionCallContext) => unknown | Promise<unknown>) | null): this;
65
+ icon(name: string | null | ((ctx: ComponentContext) => string | null | Promise<string | null>)): this;
66
+ /** Icône quand l'action est un lien de ligne de table. */
67
+ tableIcon(name: string | null): this;
68
+ private _iconButton;
69
+ /** Action de ligne réduite à son icône ; le libellé devient l'infobulle et le nom accessible (port de `iconButton()`). */
70
+ iconButton(condition?: boolean): this;
71
+ /** Icône quand l'action est un élément de menu (ActionGroup). */
72
+ groupedIcon(name: string | null): this;
73
+ requiresConfirmation(condition?: boolean): this;
74
+ modalHeading(heading: Lazy<string | null, ActionCallContext>): this;
75
+ modalDescription(description: Lazy<string | null, ActionCallContext>): this;
76
+ modalSubmitActionLabel(label: string | null): this;
77
+ modalIcon(name: string | null): this;
78
+ private _badge;
79
+ private _badgeColor;
80
+ /** Pastille affichée sur le bouton (compteur). */
81
+ badge(badge: string | number | null | (() => string | number | null | Promise<string | number | null>)): this;
82
+ badgeColor(color: string): this;
83
+ private _slideOver;
84
+ private _hasSubmit;
85
+ /** Modale en panneau latéral. */
86
+ slideOver(condition?: boolean): this;
87
+ /** `false` : modale de consultation, sans bouton de validation. */
88
+ modalSubmitAction(condition: boolean): this;
89
+ private _cancelLabel;
90
+ /** Libellé du bouton de fermeture de la modale (« Cancel » par défaut). */
91
+ modalCancelActionLabel(label: string | null): this;
92
+ private _tooltip;
93
+ private _newTab;
94
+ /** Infobulle affichée au survol. */
95
+ tooltip(text: Lazy<string | null, ComponentContext>): this;
96
+ /** Ouvre l'adresse de l'action dans un nouvel onglet. */
97
+ openUrlInNewTab(condition?: boolean): this;
98
+ private tooltipAttribute;
99
+ private _modalIconColor;
100
+ /** Couleur de l'icône de la modale ; par défaut celle de l'action. */
101
+ modalIconColor(color: string | null): this;
102
+ getColor(): string;
103
+ getLabel(ctx: ComponentContext): Promise<string>;
104
+ getIcon(ctx: ComponentContext): Promise<string | null>;
105
+ private _sort;
106
+ /** Ordre dans un menu (menu utilisateur, menu du locataire) : négatif, avant le choix du thème ou la liste des locataires. */
107
+ sort(sort: number): this;
108
+ getSort(): number;
109
+ /**
110
+ * Élément d'un menu du panel (menu utilisateur, menu du locataire) — port de l'action « grouped » à adresse.
111
+ * Seules les actions à adresse y ont leur place : le menu n'est pas un composant qui monte des actions.
112
+ */
113
+ toMenuItemHtml(ctx: ComponentContext, options?: {
114
+ navigate?: boolean;
115
+ }): Promise<string>;
116
+ /** Une adresse calculée ne se connaît qu'avec l'enregistrement : l'hôte la revérifie au montage (`resolveUrl`). */
117
+ isMountable(): boolean;
118
+ /** Adresse de l'action dans ce contexte ; une action qui en a une est un lien, jamais une action montée. */
119
+ resolveUrl(ctx: ComponentContext): string | null;
120
+ hasModal(): boolean;
121
+ /** Formulaire affiché dans la modale. Une fonction : les composants sont reconstruits à chaque requête. */
122
+ schema(components: any[] | ((schema: any) => any), columns?: Record<string, number> | number | null): this;
123
+ /** Applique le formulaire de l'action au schéma fourni par l'hôte : la fonction rend soit des composants, soit le schéma configuré. */
124
+ applySchema(schema: any): any | null;
125
+ /** Boutons ajoutés au pied de la modale, entre la validation et l'annulation. */
126
+ extraModalFooterActions(actions: Action[]): this;
127
+ /** État initial du formulaire (ex. à partir de l'enregistrement). */
128
+ fillForm(resolver: (ctx: ActionCallContext) => Record<string, unknown> | Promise<Record<string, unknown>>): this;
129
+ /** Largeur de la modale (`xs` … `7xl`). Par défaut `md` pour une confirmation, `4xl` pour un formulaire. */
130
+ modalWidth(width: string | null): this;
131
+ hasSchema(): boolean;
132
+ getFormFill(ctx: ActionCallContext): Promise<Record<string, unknown> | null>;
133
+ private getDefaultLabel;
134
+ private _mutateData;
135
+ private _before;
136
+ private _after;
137
+ /** Transforme les données validées du formulaire avant le traitement (ajouter l'auteur, normaliser une valeur…). */
138
+ mutateDataUsing(fn: (data: Record<string, unknown>, ctx: ActionCallContext) => Record<string, unknown> | Promise<Record<string, unknown>>): this;
139
+ /** Exécuté juste avant le traitement ; lever une erreur l'annule. */
140
+ before(fn: (ctx: ActionCallContext) => unknown | Promise<unknown>): this;
141
+ /** Exécuté après un traitement réussi (journal, notification supplémentaire…). */
142
+ after(fn: (ctx: ActionCallContext) => unknown | Promise<unknown>): this;
143
+ call(ctx: ActionCallContext): Promise<unknown>;
144
+ /** Élément de menu d'une action groupée de table — port de l'action « grouped » (`fi-ac-grouped-action`). Le clic passe par `filamentTable.mountAction()`, qui envoie la sélection. */
145
+ toGroupedHtml(ctx: ComponentContext, context: Record<string, unknown>): Promise<string>;
146
+ /**
147
+ * Élément de menu d'une action de ligne (dans un ActionGroup) — port de l'action « grouped ».
148
+ * Filament a deux balisages : un chemin court, et la vue Blade dès qu'un raccourci clavier est posé (ordre des classes différent).
149
+ */
150
+ toRecordGroupedHtml(ctx: ComponentContext, context: Record<string, unknown>, wireKey: string): Promise<string>;
151
+ /** Lien d'une action de ligne de table (`fi-ac-link-action`) — port de l'action « link » en petite taille. */
152
+ toLinkHtml(ctx: ComponentContext, context: Record<string, unknown>, wireKey: string): Promise<string>;
153
+ /** Fenêtre modale d'une action montée — port de filament-actions::action-modal et filament::components.modal (confirmation seulement). */
154
+ toModalHtml(ctx: ActionCallContext, index: number, content?: string | null): Promise<string>;
155
+ alpineClickHandler(handler: string | null): this;
156
+ private _livewireTarget;
157
+ private _iconAfter;
158
+ /** Cible de l'indicateur de chargement (`wire:target`), quand le clic est géré par du JavaScript. */
159
+ livewireTarget(target: string | null): this;
160
+ /** Icône (et indicateur de chargement) après le libellé : `'after'`. */
161
+ iconPosition(position: 'before' | 'after'): this;
162
+ keyBindings(bindings: string[]): this;
163
+ getName(): string;
164
+ private _authorize;
165
+ /** Condition d'autorisation, distincte de `visible` / `hidden` : non remplie, l'action est masquée et refusée par le serveur. */
166
+ authorize(condition: Lazy<boolean, ComponentContext>): this;
167
+ isVisible(ctx: ComponentContext): Promise<boolean>;
168
+ visible(condition?: Lazy<boolean, ComponentContext>): this;
169
+ /** Action affichée mais inutilisable ; le serveur refuse aussi de la monter. */
170
+ disabled(condition?: Lazy<boolean, ComponentContext>): this;
171
+ isDisabled(ctx: ComponentContext): Promise<boolean>;
172
+ /** Bouton plein — port de la vue `filament::components.button` pour une action de soumission. */
173
+ toHtml(ctx: ComponentContext, explicitMountContext?: Record<string, unknown> | null): Promise<string>;
174
+ }
175
+ /** Groupe d'actions replié dans un menu déroulant (port de ActionGroup, bouton-icône « ⋮ »). */
176
+ export declare class ActionGroup {
177
+ private actions;
178
+ constructor(actions: Action[]);
179
+ static make(actions: Action[]): ActionGroup;
180
+ getActions(): Action[];
181
+ /** `keyOf` donne la clé `wire:key` de chaque action (elle dépend de l'enregistrement). */
182
+ toHtml(ctx: ComponentContext, context: Record<string, unknown>, keyOf: (action: Action) => string): Promise<string>;
183
+ }
package/lib/index.js ADDED
@@ -0,0 +1,371 @@
1
+ // @navette-js/actions — port de filament/actions : Action (bouton plein, soumission de formulaire) pour commencer.
2
+ import { Attributes, e, evaluate, icon, js, loadingIndicator, renderHook, views, configured, configureUsing } from '@navette-js/support';
3
+ import { t } from '@navette-js/support/i18n';
4
+ import { fileURLToPath } from 'node:url';
5
+ views.namespace('filament-actions', fileURLToPath(new URL('../views', import.meta.url)));
6
+ /** Emplacements de render hook d'une modale d'action — mêmes noms que `Filament\Actions\View\ActionsRenderHook`. Donnée reçue : `data.action`. */
7
+ export const ActionsRenderHook = {
8
+ MODAL_CUSTOM_CONTENT_AFTER: 'actions::modal.custom-content.after',
9
+ MODAL_CUSTOM_CONTENT_BEFORE: 'actions::modal.custom-content.before',
10
+ MODAL_CUSTOM_CONTENT_FOOTER_AFTER: 'actions::modal.custom-content-footer.after',
11
+ MODAL_CUSTOM_CONTENT_FOOTER_BEFORE: 'actions::modal.custom-content-footer.before',
12
+ MODAL_SCHEMA_AFTER: 'actions::modal.schema.after',
13
+ MODAL_SCHEMA_BEFORE: 'actions::modal.schema.before',
14
+ };
15
+ let colorManager = null;
16
+ export const useColorManager = (manager) => { colorManager = manager; };
17
+ export class Action {
18
+ /** Réglage par défaut de toutes les instances créées ensuite par `make()` : `TextInput.configureUsing((input) => input.maxLength(255))`. */
19
+ static configureUsing(configure) { configureUsing(this, configure); }
20
+ name;
21
+ _label = null;
22
+ _color = 'primary';
23
+ _size = 'md';
24
+ _submit = null;
25
+ _hidden = false;
26
+ _visible = true;
27
+ _disabled = false;
28
+ _url = null;
29
+ _action = null;
30
+ _handler = null;
31
+ _requiresConfirmation = false;
32
+ _modalHeading = null;
33
+ _modalDescription = null;
34
+ _modalSubmitActionLabel = null;
35
+ _modalIcon = null;
36
+ _icon = null;
37
+ _alpineClick = null;
38
+ _keyBindings = [];
39
+ _schema = null;
40
+ _extraModalFooterActions = [];
41
+ _schemaColumns = {};
42
+ _fillForm = null;
43
+ _modalWidth = null;
44
+ _lazyUrl = null;
45
+ _tableIcon = null;
46
+ _groupedIcon = null;
47
+ _lazyColor = null;
48
+ _lazyIcon = null;
49
+ constructor(name) { this.name = name; }
50
+ static make(name) { return configured(new Action(name)); }
51
+ label(label) { this._label = label; return this; }
52
+ /** Une fonction rend la couleur dépendante de l'enregistrement (actions de ligne). */
53
+ color(color) { if (typeof color === 'function')
54
+ this._lazyColor = color;
55
+ else {
56
+ this._color = color;
57
+ this._lazyColor = null;
58
+ } return this; }
59
+ size(size) { this._size = size; return this; }
60
+ /** Bouton `type="submit"` qui déclenche la méthode de soumission du formulaire (`->submit('authenticate')`). */
61
+ submit(form) { this._submit = form; return this; }
62
+ hidden(condition = true) { this._hidden = condition; return this; }
63
+ /** Action de navigation : rendue en `<a href wire:navigate>`. */
64
+ url(url) { if (typeof url === 'function')
65
+ this._lazyUrl = url;
66
+ else {
67
+ this._url = url;
68
+ this._lazyUrl = null;
69
+ } return this; }
70
+ /** Une chaîne : méthode du composant hôte appelée au clic (`wire:click`). Une fonction : traitement de l'action, monté par `mountAction()` (avec modale si elle en a une). */
71
+ action(action) {
72
+ if (typeof action === 'function') {
73
+ this._handler = action;
74
+ this._action = null;
75
+ }
76
+ else {
77
+ this._action = action;
78
+ this._handler = null;
79
+ }
80
+ return this;
81
+ }
82
+ icon(name) { if (typeof name === 'function')
83
+ this._lazyIcon = name;
84
+ else {
85
+ this._icon = name;
86
+ this._lazyIcon = null;
87
+ } return this; }
88
+ /** Icône quand l'action est un lien de ligne de table. */
89
+ tableIcon(name) { this._tableIcon = name; return this; }
90
+ _iconButton = false;
91
+ /** Action de ligne réduite à son icône ; le libellé devient l'infobulle et le nom accessible (port de `iconButton()`). */
92
+ iconButton(condition = true) { this._iconButton = condition; return this; }
93
+ /** Icône quand l'action est un élément de menu (ActionGroup). */
94
+ groupedIcon(name) { this._groupedIcon = name; return this; }
95
+ requiresConfirmation(condition = true) { this._requiresConfirmation = condition; return this; }
96
+ modalHeading(heading) { this._modalHeading = heading; return this; }
97
+ modalDescription(description) { this._modalDescription = description; return this; }
98
+ modalSubmitActionLabel(label) { this._modalSubmitActionLabel = label; return this; }
99
+ modalIcon(name) { this._modalIcon = name; return this; }
100
+ _badge = null;
101
+ _badgeColor = 'primary';
102
+ /** Pastille affichée sur le bouton (compteur). */
103
+ badge(badge) { this._badge = badge; return this; }
104
+ badgeColor(color) { this._badgeColor = color; return this; }
105
+ _slideOver = false;
106
+ _hasSubmit = true;
107
+ /** Modale en panneau latéral. */
108
+ slideOver(condition = true) { this._slideOver = condition; return this; }
109
+ /** `false` : modale de consultation, sans bouton de validation. */
110
+ modalSubmitAction(condition) { this._hasSubmit = condition; return this; }
111
+ _cancelLabel = null;
112
+ /** Libellé du bouton de fermeture de la modale (« Cancel » par défaut). */
113
+ modalCancelActionLabel(label) { this._cancelLabel = label; return this; }
114
+ _tooltip = null;
115
+ _newTab = false;
116
+ /** Infobulle affichée au survol. */
117
+ tooltip(text) { this._tooltip = text; return this; }
118
+ /** Ouvre l'adresse de l'action dans un nouvel onglet. */
119
+ openUrlInNewTab(condition = true) { this._newTab = condition; return this; }
120
+ async tooltipAttribute(ctx) {
121
+ const text = await evaluate(this._tooltip, ctx);
122
+ return text ? `x-tooltip="${e(`{
123
+ content: ${js(text)},
124
+ theme: $store.theme,
125
+ allowHTML: false,
126
+ }`)}" ` : '';
127
+ }
128
+ _modalIconColor = null;
129
+ /** Couleur de l'icône de la modale ; par défaut celle de l'action. */
130
+ modalIconColor(color) { this._modalIconColor = color; return this; }
131
+ getColor() { return this._color; }
132
+ async getLabel(ctx) { return (await evaluate(this._label, ctx)) ?? this.getDefaultLabel(); }
133
+ async getIcon(ctx) { return this._lazyIcon ? this._lazyIcon(ctx) : this._icon; }
134
+ _sort = 0;
135
+ /** Ordre dans un menu (menu utilisateur, menu du locataire) : négatif, avant le choix du thème ou la liste des locataires. */
136
+ sort(sort) { this._sort = sort; return this; }
137
+ getSort() { return this._sort; }
138
+ /**
139
+ * Élément d'un menu du panel (menu utilisateur, menu du locataire) — port de l'action « grouped » à adresse.
140
+ * Seules les actions à adresse y ont leur place : le menu n'est pas un composant qui monte des actions.
141
+ */
142
+ async toMenuItemHtml(ctx, options = {}) {
143
+ if (!colorManager)
144
+ throw new Error('Aucun gestionnaire de couleurs : appelez useColorManager().');
145
+ const href = this.resolveUrl(ctx);
146
+ if (!href || !/^(\/(?!\/)|https?:\/\/)/.test(href))
147
+ return '';
148
+ const iconName = await this.getIcon(ctx);
149
+ const colors = this._color !== 'gray' && this._color !== 'primary' ? colorManager.getComponentClasses('dropdown-item', this._color) : [];
150
+ const glyph = iconName ? icon(iconName, { attributes: new Attributes().class(colors.length ? ['fi-color', `fi-color-${this._color}`] : []) }) : '';
151
+ return `<a href="${e(href)}"${this._newTab ? ' target="_blank"' : options.navigate ? ' wire:navigate' : ''} class="${['fi-dropdown-list-item', 'fi-ac-grouped-action', ...colors].join(' ')} " role="menuitem" tabindex="-1">${glyph}<span class="fi-dropdown-list-item-label">${e(await this.getLabel(ctx))}</span></a>`;
152
+ }
153
+ /** Une adresse calculée ne se connaît qu'avec l'enregistrement : l'hôte la revérifie au montage (`resolveUrl`). */
154
+ isMountable() { return (this._handler !== null || this._schema !== null) && this._url === null; }
155
+ /** Adresse de l'action dans ce contexte ; une action qui en a une est un lien, jamais une action montée. */
156
+ resolveUrl(ctx) { return this._lazyUrl ? this._lazyUrl(ctx) : this._url; }
157
+ hasModal() { return this._requiresConfirmation || this._schema !== null; }
158
+ /** Formulaire affiché dans la modale. Une fonction : les composants sont reconstruits à chaque requête. */
159
+ schema(components, columns = {}) { this._schema = typeof components === 'function' ? components : () => components; this._schemaColumns = columns; return this; }
160
+ /** Applique le formulaire de l'action au schéma fourni par l'hôte : la fonction rend soit des composants, soit le schéma configuré. */
161
+ applySchema(schema) {
162
+ if (!this._schema)
163
+ return null;
164
+ const result = this._schema(schema);
165
+ return Array.isArray(result) ? schema.columns(this._schemaColumns).components(result) : result;
166
+ }
167
+ /** Boutons ajoutés au pied de la modale, entre la validation et l'annulation. */
168
+ extraModalFooterActions(actions) { this._extraModalFooterActions = actions; return this; }
169
+ /** État initial du formulaire (ex. à partir de l'enregistrement). */
170
+ fillForm(resolver) { this._fillForm = resolver; return this; }
171
+ /** Largeur de la modale (`xs` … `7xl`). Par défaut `md` pour une confirmation, `4xl` pour un formulaire. */
172
+ modalWidth(width) { this._modalWidth = width; return this; }
173
+ hasSchema() { return this._schema !== null; }
174
+ async getFormFill(ctx) { return this._fillForm ? this._fillForm(ctx) : null; }
175
+ getDefaultLabel() { return ((s) => s.charAt(0).toUpperCase() + s.slice(1))(this.name.replace(/[_-]+/g, ' ')); }
176
+ _mutateData = null;
177
+ _before = null;
178
+ _after = null;
179
+ /** Transforme les données validées du formulaire avant le traitement (ajouter l'auteur, normaliser une valeur…). */
180
+ mutateDataUsing(fn) { this._mutateData = fn; return this; }
181
+ /** Exécuté juste avant le traitement ; lever une erreur l'annule. */
182
+ before(fn) { this._before = fn; return this; }
183
+ /** Exécuté après un traitement réussi (journal, notification supplémentaire…). */
184
+ after(fn) { this._after = fn; return this; }
185
+ async call(ctx) {
186
+ if (!this._handler)
187
+ return null;
188
+ if (this._mutateData)
189
+ ctx = { ...ctx, data: await this._mutateData(ctx.data ?? {}, ctx) };
190
+ await this._before?.(ctx);
191
+ const result = await this._handler(ctx);
192
+ await this._after?.(ctx);
193
+ return result;
194
+ }
195
+ /** Élément de menu d'une action groupée de table — port de l'action « grouped » (`fi-ac-grouped-action`). Le clic passe par `filamentTable.mountAction()`, qui envoie la sélection. */
196
+ async toGroupedHtml(ctx, context) {
197
+ if (!colorManager)
198
+ throw new Error('Aucun gestionnaire de couleurs : appelez useColorManager().');
199
+ const expression = `mountAction(${js(this.name)}, {}, ${js(context)})`;
200
+ const attributes = new Attributes()
201
+ .merge({ role: 'menuitem', tabindex: '-1', type: 'button', 'wire:loading.attr': 'aria-disabled', 'wire:loading.class': 'fi-disabled', 'wire:target': expression })
202
+ .class(['fi-ac-grouped-action', 'fi-dropdown-list-item', ...(this._color !== 'gray' ? colorManager.getComponentClasses('dropdown-item', this._color) : [])])
203
+ .set('x-cloak', 'x-cloak').set('x-show', 'getSelectedRecordsCount()').set('x-on:click', expression);
204
+ const iconHtml = this._icon ? icon(this._icon, { attributes: new Attributes({ 'wire:loading.remove.delay.default': '1', 'wire:target': expression }).class(this._color !== 'gray' ? ['fi-color', `fi-color-${this._color}`] : []) }) : '';
205
+ return `<button ${attributes.toHtml()}>${iconHtml}${loadingIndicator(new Attributes({ 'wire:loading.delay.default': '', 'wire:target': expression }))}<span class="fi-dropdown-list-item-label">${e((await evaluate(this._label, ctx)) ?? this.getDefaultLabel())}</span></button>`;
206
+ }
207
+ /**
208
+ * Élément de menu d'une action de ligne (dans un ActionGroup) — port de l'action « grouped ».
209
+ * Filament a deux balisages : un chemin court, et la vue Blade dès qu'un raccourci clavier est posé (ordre des classes différent).
210
+ */
211
+ async toRecordGroupedHtml(ctx, context, wireKey) {
212
+ if (!colorManager)
213
+ throw new Error('Aucun gestionnaire de couleurs : appelez useColorManager().');
214
+ const color = this._lazyColor ? await this._lazyColor(ctx) : this._color;
215
+ const iconName = this._lazyIcon ? await this._lazyIcon(ctx) : (this._groupedIcon ?? this._icon);
216
+ const expression = e(`mountAction(${js(this.name)}, {}, ${js(context)})`);
217
+ const colors = color !== 'gray' ? colorManager.getComponentClasses('dropdown-item', color) : [];
218
+ const label = (await evaluate(this._label, ctx)) ?? this.getDefaultLabel();
219
+ const iconHtml = iconName ? icon(iconName, { attributes: new Attributes({ 'wire:loading.remove.delay.default': '1', 'wire:target': `mountAction(${js(this.name)}, {}, ${js(context)})` }).class(color !== 'gray' ? ['fi-color', `fi-color-${color}`] : []) }) : '';
220
+ const inner = `${iconHtml}${loadingIndicator(new Attributes({ 'wire:loading.delay.default': '', 'wire:target': `mountAction(${js(this.name)}, {}, ${js(context)})` }))}<span class="fi-dropdown-list-item-label">${e(label)}</span>`;
221
+ // Action de navigation : un lien, sans montage côté serveur.
222
+ const href = this._lazyUrl ? this._lazyUrl(ctx) : this._url;
223
+ if (href) {
224
+ const plainIcon = iconName ? icon(iconName, { attributes: new Attributes().class(color !== 'gray' ? ['fi-color', `fi-color-${color}`] : []) }) : '';
225
+ // Lien externe (nouvel onglet) : pas de navigation Livewire, classes dans l'ordre de Filament.
226
+ if (this._newTab)
227
+ return `<a href="${e(href)}" target="_blank" role="menuitem" tabindex="-1" wire:key="${e(wireKey)}" class="${['fi-ac-grouped-action', 'fi-dropdown-list-item', ...colors].join(' ')}">${plainIcon}<span class="fi-dropdown-list-item-label">${e(label)}</span></a>`;
228
+ return `<a href="${e(href)}" wire:navigate wire:key="${e(wireKey)}" class="${['fi-dropdown-list-item', 'fi-ac-grouped-action', ...colors].join(' ')}" role="menuitem" tabindex="-1">${plainIcon}<span class="fi-dropdown-list-item-label">${e(label)}</span></a>`;
229
+ }
230
+ if (this._disabled !== false) {
231
+ // Désactivation conditionnelle : Filament passe alors par sa vue Blade, que l'action soit active ou non (ordre des classes différent).
232
+ const disabled = await this.isDisabled(ctx);
233
+ return `<button ${disabled ? 'aria-disabled="true" ' : ''}role="menuitem" tabindex="-1" type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled" class="${['fi-ac-grouped-action', 'fi-dropdown-list-item', disabled ? 'fi-disabled' : null, ...colors].filter(Boolean).join(' ')}" wire:click="${expression}" wire:key="${e(wireKey)}">${inner}</button>`;
234
+ }
235
+ if (this._keyBindings.length) {
236
+ return `<button x-bind:id="$id('key-bindings')" x-mousetrap.global.${this._keyBindings.map((k) => k.replace(/\+/g, '-')).join('.')}="document.getElementById($el.id)?.click()" role="menuitem" tabindex="-1" type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled" class="${['fi-ac-grouped-action', 'fi-dropdown-list-item', ...colors].join(' ')}" wire:click="${expression}" wire:key="${e(wireKey)}">${inner}</button>`;
237
+ }
238
+ return `<button type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled" wire:click="${expression}" wire:key="${e(wireKey)}" class="${['fi-dropdown-list-item', 'fi-ac-grouped-action', ...colors].join(' ')}" role="menuitem" tabindex="-1">${inner}</button>`;
239
+ }
240
+ /** Lien d'une action de ligne de table (`fi-ac-link-action`) — port de l'action « link » en petite taille. */
241
+ async toLinkHtml(ctx, context, wireKey) {
242
+ if (!colorManager)
243
+ throw new Error('Aucun gestionnaire de couleurs : appelez useColorManager().');
244
+ const color = this._lazyColor ? await this._lazyColor(ctx) : this._color;
245
+ const iconName = this._lazyIcon ? await this._lazyIcon(ctx) : (this._tableIcon ?? this._icon);
246
+ const raw = `mountAction(${js(this.name)}, {}, ${js(context)})`;
247
+ const label = (await evaluate(this._label, ctx)) ?? this.getDefaultLabel();
248
+ const classes = ['fi-ac-link-action', 'fi-link', 'fi-size-sm', ...(color !== 'gray' ? colorManager.getComponentClasses('link', color) : [])].join(' ');
249
+ if (this._iconButton) {
250
+ const disabled = this._disabled !== false && (await this.isDisabled(ctx));
251
+ const buttonClasses = ['fi-ac-icon-btn-action', 'fi-icon-btn', 'fi-size-sm', disabled ? 'fi-disabled' : null, ...(color !== 'gray' ? colorManager.getComponentClasses('icon-button', color) : [])].filter(Boolean).join(' ');
252
+ const glyph = iconName ? icon(iconName, { attributes: new Attributes({ 'wire:loading.remove.delay.default': '1', 'wire:target': raw }) }) : '';
253
+ return `<button title="${e(label)}" aria-label="${e(label)}" ${disabled ? 'disabled ' : ''}type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled" class="${buttonClasses}"${disabled ? '' : ` wire:click="${e(raw)}"`} wire:key="${e(wireKey)}">${glyph}${loadingIndicator(new Attributes({ 'wire:loading.delay.default': '', 'wire:target': raw }))}</button>`;
254
+ }
255
+ const iconHtml = iconName ? icon(iconName, { size: 'sm', attributes: new Attributes({ 'wire:loading.remove.delay.default': '1', 'wire:target': raw }) }) : '';
256
+ // Action de navigation : un lien vers la page, sans montage côté serveur.
257
+ const href = this._lazyUrl ? this._lazyUrl(ctx) : this._url;
258
+ const tooltip = await this.tooltipAttribute(ctx);
259
+ // Seules les adresses locales ou http(s) deviennent des liens.
260
+ if (href && /^(\/(?!\/)|https?:\/\/)/.test(href))
261
+ return `<a href="${e(href)}" ${this._newTab ? 'target="_blank" ' : ''}${tooltip}${this._newTab ? '' : 'wire:navigate '}wire:key="${e(wireKey)}" class="${classes}">${iconName ? icon(iconName, { size: 'sm' }) : ''}<span class="fi-link-label">${e(label)}</span></a>`;
262
+ const keys = this._keyBindings.length ? `x-bind:id="$id('key-bindings')" x-mousetrap.global.${this._keyBindings.map((k) => k.replace(/\+/g, '-')).join('.')}="document.getElementById($el.id)?.click()" ` : '';
263
+ return `<button ${keys}${tooltip}type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled" class="${classes}" wire:click="${e(raw)}" wire:key="${e(wireKey)}">${iconHtml}${loadingIndicator(new Attributes({ 'wire:loading.delay.default': '', 'wire:target': raw }), 'sm')}<span class="fi-link-label">${e(label)}</span></button>`;
264
+ }
265
+ /** Fenêtre modale d'une action montée — port de filament-actions::action-modal et filament::components.modal (confirmation seulement). */
266
+ async toModalHtml(ctx, index, content = null) {
267
+ if (!colorManager)
268
+ throw new Error('Aucun gestionnaire de couleurs : appelez useColorManager().');
269
+ const schemaCtx = { livewire: ctx.livewire, component: null, operation: null, record: ctx.record, get: () => undefined, state: undefined };
270
+ const label = (await evaluate(this._label, schemaCtx)) ?? this.getDefaultLabel();
271
+ const color = this._lazyColor ? await this._lazyColor(schemaCtx) : this._color;
272
+ const confirmation = this._requiresConfirmation;
273
+ // Comme Filament : une confirmation est centrée (md) ; un formulaire est aligné au début (4xl), sauf en petite largeur.
274
+ const width = this._modalWidth ?? (confirmation ? 'md' : '4xl');
275
+ const submitLabel = this._modalSubmitActionLabel ?? t(confirmation ? 'filament-actions::modal.actions.confirm.label' : 'filament-actions::modal.actions.submit.label');
276
+ // Même ordre que filament-actions::action-modal : contenu libre, schéma, pied de contenu, chacun entre ses deux emplacements.
277
+ const scopes = ctx.livewire.getRenderHookScopes?.() ?? [];
278
+ const hook = (name) => renderHook(name, scopes, { action: this });
279
+ const hooked = hook(ActionsRenderHook.MODAL_CUSTOM_CONTENT_BEFORE) + hook(ActionsRenderHook.MODAL_CUSTOM_CONTENT_AFTER)
280
+ + (content !== null ? hook(ActionsRenderHook.MODAL_SCHEMA_BEFORE) + content + hook(ActionsRenderHook.MODAL_SCHEMA_AFTER) : '')
281
+ + hook(ActionsRenderHook.MODAL_CUSTOM_CONTENT_FOOTER_BEFORE) + hook(ActionsRenderHook.MODAL_CUSTOM_CONTENT_FOOTER_AFTER);
282
+ if (content !== null || hooked !== '')
283
+ content = hooked;
284
+ return views.render('filament-actions::modal', {
285
+ slideOver: this._slideOver, content, confirmation, width, alignment: confirmation || ['xs', 'sm'].includes(width) ? 'center' : 'start',
286
+ livewireId: ctx.livewire.id, id: `fi-${ctx.livewire.id}-action-${index}`, index, name: this.name,
287
+ heading: (await evaluate(this._modalHeading, ctx)) ?? label,
288
+ description: (await evaluate(this._modalDescription, ctx)) ?? (this._requiresConfirmation ? t('filament-actions::modal.confirmation') : null),
289
+ iconName: this._modalIcon ?? (this._requiresConfirmation ? 'o-exclamation-triangle' : null), color,
290
+ cancel: await Action.make('cancel').label(this._cancelLabel ?? t('filament-actions::modal.actions.cancel.label')).color('gray').alpineClickHandler('close()').toHtml(schemaCtx),
291
+ // Comme Filament : le bouton de validation d'une action grise est en couleur primaire.
292
+ submit: !this._hasSubmit ? '' : (await Action.make('submit').label(submitLabel).color(color === 'gray' ? 'primary' : color).submit('callMountedAction').toHtml(schemaCtx)) + (await Promise.all(this._extraModalFooterActions.map((a) => a.toHtml(schemaCtx)))).join(''),
293
+ });
294
+ }
295
+ alpineClickHandler(handler) { this._alpineClick = handler; return this; }
296
+ _livewireTarget = null;
297
+ _iconAfter = false;
298
+ /** Cible de l'indicateur de chargement (`wire:target`), quand le clic est géré par du JavaScript. */
299
+ livewireTarget(target) { this._livewireTarget = target; return this; }
300
+ /** Icône (et indicateur de chargement) après le libellé : `'after'`. */
301
+ iconPosition(position) { this._iconAfter = position === 'after'; return this; }
302
+ keyBindings(bindings) { this._keyBindings = bindings; return this; }
303
+ getName() { return this.name; }
304
+ _authorize = true;
305
+ /** Condition d'autorisation, distincte de `visible` / `hidden` : non remplie, l'action est masquée et refusée par le serveur. */
306
+ authorize(condition) { this._authorize = condition; return this; }
307
+ async isVisible(ctx) { return !(await evaluate(this._hidden, ctx)) && (await evaluate(this._visible, ctx)) && Boolean(await evaluate(this._authorize, ctx)); }
308
+ visible(condition = true) { this._visible = condition; return this; }
309
+ /** Action affichée mais inutilisable ; le serveur refuse aussi de la monter. */
310
+ disabled(condition = true) { this._disabled = condition; return this; }
311
+ async isDisabled(ctx) { return evaluate(this._disabled, ctx); }
312
+ /** Bouton plein — port de la vue `filament::components.button` pour une action de soumission. */
313
+ async toHtml(ctx, explicitMountContext = null) {
314
+ if (!colorManager)
315
+ throw new Error('Aucun gestionnaire de couleurs : appelez useColorManager().');
316
+ const linkUrl = this._lazyUrl ? this._lazyUrl(ctx) : this._url;
317
+ if (linkUrl !== null && /^(\/(?!\/)|https?:\/\/)/.test(linkUrl)) {
318
+ const link = new Attributes().class(['fi-ac-btn-action', 'fi-btn', `fi-size-${this._size}`, ...(this._color !== 'gray' ? colorManager.getComponentClasses('button', this._color) : [])]);
319
+ const linkIcon = this._icon ? icon(this._icon) : '';
320
+ const badge = typeof this._badge === 'function' ? await this._badge() : this._badge;
321
+ const badgeHtml = badge !== null && badge !== '' ? `<div class="fi-btn-badge-ctn"><span class="${['fi-color', `fi-color-${this._badgeColor}`, ...colorManager.getComponentClasses('badge', this._badgeColor).filter((c) => !c.startsWith('fi-color')), 'fi-badge', 'fi-size-xs'].join(' ')}">${e(String(badge))}</span></div>` : '';
322
+ return `<a href="${e(linkUrl)}" ${this._newTab ? 'target="_blank" ' : ''}${await this.tooltipAttribute(ctx)}${this._newTab ? '' : 'wire:navigate '}${link.toHtml()}>${linkIcon}${e((await evaluate(this._label, ctx)) ?? this.getDefaultLabel())}${badgeHtml}</a>`;
323
+ }
324
+ const isSubmit = this._submit !== null;
325
+ // Action à traitement : le clic monte l'action côté serveur, avec le contexte fourni par l'hôte (ex. l'enregistrement).
326
+ const mountContext = explicitMountContext ?? ctx.livewire?.getActionMountContext?.() ?? {};
327
+ // Sans contexte, Filament émet la forme courte `mountAction('nom')`.
328
+ const mountExpression = this._handler ? (Object.keys(mountContext).length ? `mountAction(${js(this.name)}, {}, ${js(mountContext)})` : `mountAction(${js(this.name)})`) : null;
329
+ const attributes = new Attributes()
330
+ .merge({
331
+ type: isSubmit ? 'submit' : 'button',
332
+ 'wire:loading.attr': 'aria-disabled',
333
+ 'wire:loading.class': 'fi-disabled',
334
+ 'wire:target': this._livewireTarget ?? this._submit,
335
+ 'x-bind:disabled': isSubmit ? 'isProcessing' : null,
336
+ })
337
+ .class(['fi-ac-btn-action', 'fi-btn', `fi-size-${this._size}`, ...colorManager.getComponentClasses('button', this._color)])
338
+ .when(this._action, (a) => a.set('wire:click', this._action))
339
+ .when(mountExpression, (a) => a.set('wire:click', mountExpression))
340
+ .when(this._alpineClick, (a) => a.set('x-on:click', this._alpineClick));
341
+ const wireTarget = this._livewireTarget ?? this._submit ?? this._action ?? mountExpression;
342
+ const keyBindings = this._keyBindings.length ? `x-bind:id="$id('key-bindings')" x-mousetrap.global.${this._keyBindings.map((k) => k.replace(/\+/g, '-')).join('.')}="document.getElementById($el.id)?.click()" ` : '';
343
+ return views.render('filament-actions::button', {
344
+ attributes: attributes.toHtml(), isSubmit, keyBindings, iconAfter: this._iconAfter, hasWireLoader: wireTarget !== null,
345
+ label: (await evaluate(this._label, ctx)) ?? this.getDefaultLabel(),
346
+ wireLoader: loadingIndicator(new Attributes({ 'wire:loading.delay.default': '', 'wire:target': wireTarget })),
347
+ iconHtml: this._icon ? icon(this._icon, { attributes: new Attributes({ 'wire:loading.remove.delay.default': '1', 'wire:target': wireTarget }) }) : '',
348
+ formLoader: loadingIndicator(new Attributes({ 'x-cloak': 'x-cloak', 'x-show': 'isProcessing' })),
349
+ });
350
+ }
351
+ }
352
+ /** Groupe d'actions replié dans un menu déroulant (port de ActionGroup, bouton-icône « ⋮ »). */
353
+ export class ActionGroup {
354
+ actions;
355
+ constructor(actions) { this.actions = actions; }
356
+ static make(actions) { return configured(new ActionGroup(actions)); }
357
+ getActions() { return this.actions; }
358
+ /** `keyOf` donne la clé `wire:key` de chaque action (elle dépend de l'enregistrement). */
359
+ async toHtml(ctx, context, keyOf) {
360
+ if (!colorManager)
361
+ throw new Error('Aucun gestionnaire de couleurs : appelez useColorManager().');
362
+ const items = [];
363
+ for (const action of this.actions)
364
+ if (await action.isVisible(ctx))
365
+ items.push(await action.toRecordGroupedHtml(ctx, context, keyOf(action)));
366
+ if (!items.length)
367
+ return '';
368
+ const label = e(t('filament-actions::group.trigger.label'));
369
+ return `<div x-data="filamentDropdown" class="fi-dropdown"><div x-on:mousedown="if ($event.button === 0) toggle($event)" class="fi-dropdown-trigger"><button title="${label}" aria-label="${label}" type="button" wire:loading.attr="aria-disabled" wire:loading.class="fi-disabled" class="${['fi-ac-icon-btn-group', 'fi-icon-btn', 'fi-size-md', ...colorManager.getComponentClasses('icon-button', 'primary')].join(' ')}">${icon('m-ellipsis-vertical')}</button></div><div x-cloak x-float.placement.bottom-end.flip.teleport.offset="{ offset: 8 }" x-ref="panel" x-transition:enter-start="fi-opacity-0" x-transition:leave-end="fi-opacity-0" role="menu" class="fi-dropdown-panel"><div class="fi-dropdown-list" role="group">${items.join(' ')}</div></div></div>`;
370
+ }
371
+ }
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@navette-js/actions",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0-beta.1",
4
+ "description": "Actions and modals for Navette, the Node.js port of Filament.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./lib/index.d.ts",
10
+ "default": "./lib/index.js"
11
+ }
12
+ },
13
+ "types": "./lib/index.d.ts",
14
+ "files": [
15
+ "lib",
16
+ "views",
17
+ "LICENSE",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=24"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/NavetteJS/navette.git",
26
+ "directory": "packages/actions"
27
+ },
28
+ "homepage": "https://github.com/NavetteJS/navette#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/NavetteJS/navette/issues"
31
+ },
32
+ "keywords": [
33
+ "navette",
34
+ "filament",
35
+ "admin",
36
+ "panel",
37
+ "livewire",
38
+ "express",
39
+ "node"
40
+ ],
41
+ "dependencies": {
42
+ "@navette-js/support": "1.0.0-beta.1",
43
+ "@navette-js/schemas": "1.0.0-beta.1"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "tag": "beta"
48
+ }
49
+ }
@@ -0,0 +1,13 @@
1
+ <%# Bouton d'action — port de la vue Blade filament::components.button %>
2
+ <button <%- keyBindings %><% if (isSubmit) { %>x-data="filamentFormButton" x-bind:class="{ 'fi-processing': isProcessing }" <% } %><%- attributes %>>
3
+ <% if (!iconAfter) { %><%- iconHtml %><% if (hasWireLoader) { %><%- wireLoader %><% } %><% if (isSubmit) { %><%- formLoader %><% } %><% } %>
4
+ <% if (isSubmit) { %>
5
+ <span x-show="! isProcessing">
6
+ <%= label %>
7
+ </span>
8
+ <span x-cloak x-show="isProcessing" x-text="processingMessage"></span>
9
+ <% } else { %>
10
+ <%= label %>
11
+ <% } %>
12
+ <% if (iconAfter) { %><%- iconHtml %><% if (hasWireLoader) { %><%- wireLoader %><% } %><% if (isSubmit) { %><%- formLoader %><% } %><% } %>
13
+ </button>
@@ -0,0 +1,55 @@
1
+ <%# Modale d'une action montée — port de filament-actions::action-modal et filament::components.modal (modale de confirmation, centrée, largeur md) %>
2
+ <div wire:partial="action-modals.<%- index %>"<% if (description) { %> aria-describedby="<%= id %>.description"<% } %> aria-labelledby="<%= id %>.heading" aria-modal="true" id="<%= id %>" role="<%- confirmation ? 'alertdialog' : 'dialog' %>" tabindex="-1" x-data="filamentModal({
3
+ id: <%- js(id) %>,
4
+ isScrollLocked: true,
5
+ shouldRestoreFocus: false,
6
+ })" data-fi-modal-id="<%= id %>" x-on:close-modal.window="if (($event.detail.id === <%- js(id) %>) && isOpen) close()" x-on:close-modal-quietly.window="if (($event.detail.id === <%- js(id) %>) && isOpen) closeQuietly()" x-on:open-modal.window="if (($event.detail.id === <%- js(id) %>) && (! isOpen)) open()" x-bind:class="{
7
+ 'fi-modal-open': isOpen,
8
+ }" x-cloak x-show="isOpen" class="<%- locals.slideOver ? 'fi-modal fi-modal-slide-over fi-absolute-positioning-context' : 'fi-modal fi-absolute-positioning-context' %>" wire:key="<%= livewireId %>.actions.<%= name %>.modal" x-on:modal-closed="if ($event.detail.id === <%= js(id) %>) $wire.unmountAction(false)">
9
+ <div aria-hidden="true" x-show="isOpen" x-transition.duration.300ms.opacity class="fi-modal-close-overlay"></div>
10
+
11
+ <div tabindex="-1" x-on:click.self="$dispatch(<%= js('close-modal') %>, { id: <%= js(id) %> })" x-trap.noreturn="isTrapActive" class="fi-modal-window-ctn fi-clickable">
12
+ <form x-on:keydown.window.escape="if (isTopmost()) $dispatch(<%= js('close-modal') %>, { id: <%= js(id) %> })" x-show="isWindowVisible" x-transition:enter="fi-transition-enter" x-transition:leave="fi-transition-leave" x-transition:enter-start="fi-transition-enter-start" x-transition:enter-end="fi-transition-enter-end" x-transition:leave-start="fi-transition-leave-start" x-transition:leave-end="fi-transition-leave-end" wire:submit.prevent="callMountedAction" wire:key="<%= livewireId %>.modal.<%= id %>.window" class="<%- cls(['fi-modal-window', 'fi-modal-window-has-close-btn', { 'fi-modal-window-has-content': content !== null }, 'fi-modal-window-has-footer', { 'fi-modal-window-has-icon': iconName }, 'fi-align-' + alignment, 'fi-width-' + width]) %>">
13
+ <div wire:key="<%= livewireId %>.modal.<%= id %>.header" class="fi-modal-header">
14
+ <%- view('filament::components.icon-button', {
15
+ iconName: 'o-x-mark', iconSize: 'lg', label: t('filament::components/modal.actions.close.label'), className: 'fi-modal-close-btn',
16
+ extra: attrs({ tabindex: '-1' }), after: attrs({ 'x-on:click': "$dispatch('close-modal', { id: " + js(id) + ' })' }),
17
+ }) %>
18
+
19
+ <% if (iconName) { %>
20
+ <div class="fi-modal-icon-ctn">
21
+ <div class="<%- cls([...(color !== 'gray' ? ['fi-color', 'fi-color-' + color] : []), 'fi-modal-icon-bg']) %>">
22
+ <%- icon(iconName, { size: 'lg' }) %>
23
+ </div>
24
+ </div>
25
+ <% } %>
26
+
27
+ <div>
28
+ <h2 id="<%= id %>.heading" class="fi-modal-heading">
29
+ <%= heading %>
30
+ </h2>
31
+
32
+ <% if (description) { %>
33
+ <p id="<%= id %>.description" class="fi-modal-description">
34
+ <%= description %>
35
+ </p>
36
+ <% } %>
37
+ </div>
38
+ </div>
39
+
40
+ <%# Les six emplacements de ActionsRenderHook sont assemblés dans `content` par Action.toModalHtml(). %>
41
+ <% if (content !== null) { %>
42
+ <div wire:key="<%= livewireId %>.modal.<%= id %>.content" class="fi-modal-content">
43
+ <%- content %>
44
+ </div>
45
+ <% } %>
46
+
47
+ <div wire:key="<%= livewireId %>.modal.<%= id %>.footer" class="fi-modal-footer fi-align-<%- confirmation ? 'center' : 'start' %>">
48
+ <div class="fi-modal-footer-actions">
49
+ <%# Confirmation : annuler puis confirmer ; formulaire : valider puis annuler (comme Filament) %>
50
+ <%- confirmation ? cancel + submit : submit + cancel %>
51
+ </div>
52
+ </div>
53
+ </form>
54
+ </div>
55
+ </div>