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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @navette-js/forms
2
2
 
3
- Form fields for Navette, the Node.js port of Filament.
3
+ Form fields and validation for Navette admin panels.
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
 
package/lib/builder.d.ts CHANGED
@@ -2,13 +2,13 @@ import { type ColorManager } from '@navette-js/support';
2
2
  import { Action } from '@navette-js/actions';
3
3
  import { Schema, type Component } from '@navette-js/schemas';
4
4
  import { Field } from './index.ts';
5
- /** Clé d'élément acceptable : les clés viennent du navigateur. */
5
+ /** Whether an item key is acceptable: keys come from the browser. */
6
6
  export declare const isItemKey: (key: string) => boolean;
7
- /** Clé courte à la manière de `Str::random(4)`. */
7
+ /** Random four-character alphanumeric key. */
8
8
  export declare const shortKey: () => string;
9
- /** Écrit une valeur à un chemin d'état du composant hôte. */
9
+ /** Writes a value at a state path of the host component. */
10
10
  export declare function writeState(target: any, segments: string[], value: unknown): void;
11
- /** Bouton-icône d'action de composant (même balisage que Filament : icône, puis indicateur de chargement). */
11
+ /** Icon button for a component action: the icon, then a loading indicator. */
12
12
  export declare function iconActionButton(options: {
13
13
  label: string;
14
14
  icon: string;
@@ -18,14 +18,14 @@ export declare function iconActionButton(options: {
18
18
  disabled?: boolean;
19
19
  inHeader?: boolean;
20
20
  }): string;
21
- /** Gestionnaire de couleurs du panel de l'hôte (les pages et widgets l'exposent par `panel`). */
21
+ /** Color manager of the host's panel (pages and widgets expose it through `panel`). */
22
22
  export declare const colorsOf: (livewire: unknown) => ColorManager | null;
23
23
  type BlockContext = {
24
24
  builder: Builder;
25
25
  itemKey: string;
26
26
  data: Record<string, unknown>;
27
27
  };
28
- /** Type d'élément d'un Builder (port de Builder\Block). */
28
+ /** Item type of a Builder. */
29
29
  export declare class Block {
30
30
  readonly name: string;
31
31
  private _label;
@@ -36,9 +36,9 @@ export declare class Block {
36
36
  static make(name: string): Block;
37
37
  label(label: string | null): this;
38
38
  icon(name: string | null): this;
39
- /** Composants d'un élément : une fonction, appelée pour chaque élément (ses données courantes sont fournies). */
39
+ /** Item components: a function called for each item (with its current data). */
40
40
  schema(factory: (ctx: BlockContext) => Component[]): this;
41
- /** Données d'un nouvel élément quand elles ne se déduisent pas des valeurs par défaut des champs (champs dépendant d'un autre). */
41
+ /** Data of a new item when it cannot be derived from the fields' defaults (fields depending on another). */
42
42
  defaultData(factory: (builder: Builder) => Record<string, unknown>): this;
43
43
  getLabel(): string;
44
44
  getIcon(): string | null;
@@ -73,53 +73,53 @@ export declare class Builder extends Field {
73
73
  blockPickerColumns(columns?: number | null): this;
74
74
  blockPickerWidth(width: string | null): this;
75
75
  labelBetweenItems(label: string | null): this;
76
- /** Sans en-tête, l'élément n'affiche que son contenu : ses actions sont alors à la charge du bloc. */
76
+ /** Without a header, an item only shows its content: its actions are then up to the block. */
77
77
  blockHeaders(condition?: boolean): this;
78
78
  cloneable(condition?: boolean): this;
79
- /** Numéro de l'élément après le libellé du bloc (actif par défaut, comme Filament). */
79
+ /** Item number after the block label (on by default). */
80
80
  blockNumbers(condition?: boolean): this;
81
- /** Icône du bloc dans l'en-tête de l'élément (désactivée par défaut, comme Filament). */
81
+ /** Block icon in the item header (off by default). */
82
82
  blockIcons(condition?: boolean): this;
83
- /** Balise du libellé d'en-tête (`h2` par défaut ; Filament la déduit du niveau de titre du schéma). */
83
+ /** Tag of the header label (`h2` by default). */
84
84
  blockLabelHeadingTag(tag: 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p'): this;
85
- /** Sélecteur « Insérer entre les blocs » entre deux éléments (actif par défaut, comme Filament). */
85
+ /** "Insert between blocks" picker between two items (on by default). */
86
86
  addBetweenAction(condition?: boolean): this;
87
87
  addBetweenActionLabel(label: string | null): this;
88
88
  addActionLabel(label: string | null): this;
89
89
  addActionIcon(name: string | null): this;
90
90
  addActionAlignment(alignment: 'start' | 'center' | 'end' | null): this;
91
91
  maxItems(count: number): this;
92
- /** Classe ajoutée à la racine (ex. `fi-fo-builder-not-contained`). */
92
+ /** Class added to the root (e.g. `fi-fo-builder-not-contained`). */
93
93
  extraClass(name: string | null): this;
94
94
  generateUuidUsing(generator: () => string): this;
95
95
  getBlocks(): Block[];
96
96
  getBlock(name: unknown): Block | undefined;
97
- /** Blocs proposés dans le sélecteur (une sous-classe peut en retirer). */
97
+ /** Blocks offered in the picker (a subclass may remove some). */
98
98
  getBlockPickerBlocks(): Block[];
99
- /** Éléments bien formés : clé simple, type déclaré, données en objet. Le reste de ce qu'envoie le navigateur est ignoré. */
99
+ /** Well-formed items only: simple key, declared type, object data. Anything else the browser sends is ignored. */
100
100
  getItems(): Record<string, Item>;
101
101
  protected setItems(items: Record<string, Item>): void;
102
102
  getDefaultState(): unknown;
103
103
  /**
104
- * Valeur enregistrée (liste `[{ type, data }]`, comme Filament, éventuellement en JSON) vers l'état du formulaire (table par clé).
105
- * Les champs de chaque bloc hydratent leur propre valeur.
104
+ * Stored value (a `[{ type, data }]` list, possibly as JSON) to form state (a map by key).
105
+ * Each block's fields hydrate their own value.
106
106
  */
107
107
  hydrateState(value: unknown): unknown;
108
108
  private _storedAsList;
109
- /** `false` : la valeur enregistrée reste la table par clé de l'état (filtres), au lieu de la liste `[{ type, data }]`. */
109
+ /** `false`: the stored value stays the keyed map of the state (filters) instead of a `[{ type, data }]` list. */
110
110
  storedAsList(condition?: boolean): this;
111
111
  finalizeDehydratedState(state: unknown): unknown;
112
112
  dehydrateState(): unknown;
113
- /** Schéma d'un élément : clé `<builder>.<élément>.data`, état au même chemin (comme Filament). */
113
+ /** Item schema: key `<builder>.<item>.data`, state at the same path. */
114
114
  protected itemSchema(itemKey: string, item: Item): Schema;
115
115
  getChildSchemas(): Schema[];
116
116
  protected newItem(block: Block): Item;
117
- /** Une sous-classe peut refuser l'ajout (plafond de règles, par exemple). */
117
+ /** A subclass may refuse adding (a rule cap, for instance). */
118
118
  protected canAdd(_block: Block): boolean;
119
119
  getActions(): Action[];
120
- /** Copie d'un élément ; une sous-classe renouvelle les clés des éléments imbriqués. */
120
+ /** Copy of an item; a subclass renews the keys of nested items. */
121
121
  protected cloneItem(item: Item): Item;
122
- /** Expression `mountAction(...)` d'une action de ce composant. */
122
+ /** `mountAction(...)` expression for an action of this component. */
123
123
  actionExpression(action: string, args: Record<string, unknown> | null): string;
124
124
  private blockPickerHtml;
125
125
  private _labelText;
package/lib/builder.js CHANGED
@@ -1,7 +1,6 @@
1
- // @navette-js/forms/builder — port (partiel) de Filament\Forms\Components\Builder : une liste d'éléments dont chacun est un « bloc »
2
- // choisi dans un menu. L'état est une table `clé d'élément → { type, data }` ; l'ordre des clés est l'ordre d'affichage.
3
- // Portés : blocs, sélecteur de bloc, ajout / suppression / duplication / déplacement / réordonnancement, libellé entre éléments,
4
- // en-têtes de bloc (libellé et actions). Non portés : aperçus de bloc, repli, recherche dans le sélecteur, ajout entre éléments.
1
+ // @navette-js/forms/builder — a list of items, each one a "block" picked from a menu.
2
+ // The state is a map `item key → { type, data }`; key order is display order.
3
+ // Not supported yet: block previews, collapsing, searching the block picker.
5
4
  import { Attributes, cls, e, icon, js, loadingIndicator, views, configured } from '@navette-js/support';
6
5
  import { t } from '@navette-js/support/i18n';
7
6
  import { Action } from '@navette-js/actions';
@@ -10,11 +9,11 @@ import { createHash, randomBytes, randomUUID } from 'node:crypto';
10
9
  import { Field, fillSchema } from "./index.js";
11
10
  const asset = (name) => views.render('filament-forms::asset', { path: name }).trim();
12
11
  const ITEM_KEY = /^[A-Za-z0-9_-]{1,64}$/;
13
- /** Clé d'élément acceptable : les clés viennent du navigateur. */
12
+ /** Whether an item key is acceptable: keys come from the browser. */
14
13
  export const isItemKey = (key) => ITEM_KEY.test(key) && key !== '__proto__' && key !== 'constructor' && key !== 'prototype';
15
- /** Clé courte à la manière de `Str::random(4)`. */
14
+ /** Random four-character alphanumeric key. */
16
15
  export const shortKey = () => Array.from(randomBytes(4), (b) => 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'[b % 62]).join('');
17
- /** Écrit une valeur à un chemin d'état du composant hôte. */
16
+ /** Writes a value at a state path of the host component. */
18
17
  export function writeState(target, segments, value) {
19
18
  let node = target;
20
19
  for (const key of segments.slice(0, -1)) {
@@ -24,9 +23,9 @@ export function writeState(target, segments, value) {
24
23
  }
25
24
  node[segments[segments.length - 1]] = value;
26
25
  }
27
- /** Bouton-icône d'action de composant (même balisage que Filament : icône, puis indicateur de chargement). */
26
+ /** Icon button for a component action: the icon, then a loading indicator. */
28
27
  export function iconActionButton(options) {
29
- // En-tête d'élément : Filament place `tabindex` avant `type` et la classe avant les attributs ajoutés.
28
+ // Item header buttons put `tabindex` before `type`, and the class before the extra attributes.
30
29
  const { tabindex = null, ...extra } = options.attributes ?? {};
31
30
  const attributes = new Attributes(options.inHeader ? {
32
31
  title: options.label, 'aria-disabled': options.disabled ? 'true' : null, 'aria-label': options.label, disabled: options.disabled ? 'disabled' : null, tabindex, type: 'button',
@@ -37,9 +36,9 @@ export function iconActionButton(options) {
37
36
  }).class(['fi-ac-icon-btn-action', 'fi-icon-btn', { 'fi-disabled': options.disabled === true }, 'fi-size-sm', ...(options.colorClasses ?? [])]);
38
37
  return `<button ${attributes.toHtml()}>${icon(options.icon, { attributes: new Attributes({ 'wire:loading.remove.delay.default': '1', 'wire:target': options.expression }) })}${loadingIndicator(new Attributes({ 'wire:loading.delay.default': '', 'wire:target': options.expression }))}</button>`;
39
38
  }
40
- /** Gestionnaire de couleurs du panel de l'hôte (les pages et widgets l'exposent par `panel`). */
39
+ /** Color manager of the host's panel (pages and widgets expose it through `panel`). */
41
40
  export const colorsOf = (livewire) => livewire?.panel?.colors ?? null;
42
- /** Type d'élément d'un Builder (port de Builder\Block). */
41
+ /** Item type of a Builder. */
43
42
  export class Block {
44
43
  name;
45
44
  _label = null;
@@ -50,16 +49,16 @@ export class Block {
50
49
  static make(name) { return configured(new Block(name)); }
51
50
  label(label) { this._label = label; return this; }
52
51
  icon(name) { this._icon = name; return this; }
53
- /** Composants d'un élément : une fonction, appelée pour chaque élément (ses données courantes sont fournies). */
52
+ /** Item components: a function called for each item (with its current data). */
54
53
  schema(factory) { this._schema = factory; return this; }
55
- /** Données d'un nouvel élément quand elles ne se déduisent pas des valeurs par défaut des champs (champs dépendant d'un autre). */
54
+ /** Data of a new item when it cannot be derived from the fields' defaults (fields depending on another). */
56
55
  defaultData(factory) { this._defaults = factory; return this; }
57
56
  getLabel() { return this._label ?? this.name.replace(/[_-]+/g, ' ').replace(/^./, (c) => c.toUpperCase()); }
58
57
  getIcon() { return this._icon; }
59
58
  getComponents(ctx) { return this._schema(ctx); }
60
59
  getDefaults(builder) { return this._defaults ? this._defaults(builder) : null; }
61
60
  }
62
- /** Empreinte du sélecteur de bloc : Filament y met `md5(serialize([largeur, placement, a des blocs, recherche]))`. */
61
+ /** Precomputed block picker key hashes for common `width|placement|has blocks|searchable` signatures; others hash the signature itself. */
63
62
  const PICKER_HASHES = { 'fi-width-md|bottom-start|1|0': '8b2521a32ddb82abb3a5f4fd214c90b1', '||1|0': '9c288f69c73367423cc1eb48b3e9a82b' };
64
63
  export class Builder extends Field {
65
64
  _blocks = [];
@@ -85,31 +84,30 @@ export class Builder extends Field {
85
84
  blockPickerColumns(columns = 2) { this._pickerColumns = columns; return this; }
86
85
  blockPickerWidth(width) { this._pickerWidth = width; return this; }
87
86
  labelBetweenItems(label) { this._labelBetweenItems = label; return this; }
88
- /** Sans en-tête, l'élément n'affiche que son contenu : ses actions sont alors à la charge du bloc. */
87
+ /** Without a header, an item only shows its content: its actions are then up to the block. */
89
88
  blockHeaders(condition = true) { this._blockHeaders = condition; return this; }
90
89
  cloneable(condition = true) { this._cloneable = condition; return this; }
91
- /** Numéro de l'élément après le libellé du bloc (actif par défaut, comme Filament). */
90
+ /** Item number after the block label (on by default). */
92
91
  blockNumbers(condition = true) { this._blockNumbers = condition; return this; }
93
- /** Icône du bloc dans l'en-tête de l'élément (désactivée par défaut, comme Filament). */
92
+ /** Block icon in the item header (off by default). */
94
93
  blockIcons(condition = true) { this._blockIcons = condition; return this; }
95
- /** Balise du libellé d'en-tête (`h2` par défaut ; Filament la déduit du niveau de titre du schéma). */
94
+ /** Tag of the header label (`h2` by default). */
96
95
  blockLabelHeadingTag(tag) { this._headingTag = tag; return this; }
97
- /** Sélecteur « Insérer entre les blocs » entre deux éléments (actif par défaut, comme Filament). */
96
+ /** "Insert between blocks" picker between two items (on by default). */
98
97
  addBetweenAction(condition = true) { this._addBetween = condition; return this; }
99
98
  addBetweenActionLabel(label) { this._addBetweenLabel = label; return this; }
100
99
  addActionLabel(label) { this._addLabel = label; return this; }
101
100
  addActionIcon(name) { this._addIcon = name; return this; }
102
101
  addActionAlignment(alignment) { this._addAlignment = alignment; return this; }
103
102
  maxItems(count) { this._maxItems = count; return this; }
104
- /** Classe ajoutée à la racine (ex. `fi-fo-builder-not-contained`). */
103
+ /** Class added to the root (e.g. `fi-fo-builder-not-contained`). */
105
104
  extraClass(name) { this._extraClass = name; return this; }
106
105
  generateUuidUsing(generator) { this._uuid = generator; return this; }
107
106
  getBlocks() { return (this._resolvedBlocks ??= typeof this._blocks === 'function' ? this._blocks() : this._blocks); }
108
107
  getBlock(name) { return typeof name === 'string' ? this.getBlocks().find((block) => block.name === name) : undefined; }
109
- /** Blocs proposés dans le sélecteur (une sous-classe peut en retirer). */
108
+ /** Blocks offered in the picker (a subclass may remove some). */
110
109
  getBlockPickerBlocks() { return this.getBlocks(); }
111
- // --- état
112
- /** Éléments bien formés : clé simple, type déclaré, données en objet. Le reste de ce qu'envoie le navigateur est ignoré. */
110
+ /** Well-formed items only: simple key, declared type, object data. Anything else the browser sends is ignored. */
113
111
  getItems() {
114
112
  const state = this.context().state;
115
113
  if (!state || typeof state !== 'object' || Array.isArray(state))
@@ -125,8 +123,8 @@ export class Builder extends Field {
125
123
  setItems(items) { writeState(this.livewire, this.getStatePath().split('.'), items); }
126
124
  getDefaultState() { return {}; }
127
125
  /**
128
- * Valeur enregistrée (liste `[{ type, data }]`, comme Filament, éventuellement en JSON) vers l'état du formulaire (table par clé).
129
- * Les champs de chaque bloc hydratent leur propre valeur.
126
+ * Stored value (a `[{ type, data }]` list, possibly as JSON) to form state (a map by key).
127
+ * Each block's fields hydrate their own value.
130
128
  */
131
129
  hydrateState(value) {
132
130
  if (typeof value === 'string') {
@@ -156,7 +154,7 @@ export class Builder extends Field {
156
154
  return items;
157
155
  }
158
156
  _storedAsList = true;
159
- /** `false` : la valeur enregistrée reste la table par clé de l'état (filtres), au lieu de la liste `[{ type, data }]`. */
157
+ /** `false`: the stored value stays the keyed map of the state (filters) instead of a `[{ type, data }]` list. */
160
158
  storedAsList(condition = true) { this._storedAsList = condition; return this; }
161
159
  finalizeDehydratedState(state) {
162
160
  if (!this._storedAsList || !state || typeof state !== 'object' || Array.isArray(state))
@@ -164,7 +162,7 @@ export class Builder extends Field {
164
162
  return Object.values(state);
165
163
  }
166
164
  dehydrateState() { return Object.fromEntries(Object.entries(this.getItems()).map(([key, item]) => [key, { type: item.type, data: {} }])); }
167
- /** Schéma d'un élément : clé `<builder>.<élément>.data`, état au même chemin (comme Filament). */
165
+ /** Item schema: key `<builder>.<item>.data`, state at the same path. */
168
166
  itemSchema(itemKey, item) {
169
167
  const container = this.container;
170
168
  return new Schema(container.livewire).key(`${this.getKey()}.${itemKey}.data`).statePath(`${this.getStatePath()}.${itemKey}.data`).operation(container.getOperation()).record(container.getRecord()).root(false)
@@ -180,9 +178,8 @@ export class Builder extends Field {
180
178
  fillSchema(this.itemSchema('new', item), data);
181
179
  return item;
182
180
  }
183
- /** Une sous-classe peut refuser l'ajout (plafond de règles, par exemple). */
181
+ /** A subclass may refuse adding (a rule cap, for instance). */
184
182
  canAdd(_block) { return Object.keys(this.getItems()).length < this._maxItems; }
185
- // --- actions du composant
186
183
  getActions() {
187
184
  const insert = (items, after, key, item) => {
188
185
  if (after === null || !Object.hasOwn(items, after))
@@ -225,7 +222,7 @@ export class Builder extends Field {
225
222
  }),
226
223
  Action.make('moveUp').action(move(-1)),
227
224
  Action.make('moveDown').action(move(1)),
228
- // Glisser-déposer : le nouvel ordre n'est accepté que si c'est une permutation des clés existantes.
225
+ // Drag and drop: the new order is only accepted if it is a permutation of the existing keys.
229
226
  Action.make('reorder').action(({ arguments: args }) => {
230
227
  const items = this.getItems();
231
228
  const keys = Object.keys(items);
@@ -236,9 +233,9 @@ export class Builder extends Field {
236
233
  }),
237
234
  ];
238
235
  }
239
- /** Copie d'un élément ; une sous-classe renouvelle les clés des éléments imbriqués. */
236
+ /** Copy of an item; a subclass renews the keys of nested items. */
240
237
  cloneItem(item) { return JSON.parse(JSON.stringify(item)); }
241
- /** Expression `mountAction(...)` d'une action de ce composant. */
238
+ /** `mountAction(...)` expression for an action of this component. */
242
239
  actionExpression(action, args) {
243
240
  const context = { ...(this.livewire.getActionMountContext?.() ?? {}), schemaComponent: this.getKey() };
244
241
  return `mountAction(${js(action)}, ${args ? js(args) : '{}'}, ${js(context)})`;