x4js 2.3.11 → 2.3.12

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
@@ -7,7 +7,19 @@ see [X4 home page](https://x4js.org)
7
7
  ## API Documentation
8
8
  see [GitHib](https://rlibre.github.io/x4/index.html)
9
9
 
10
- see [AI context](./aicontext.md)
10
+ ## Using x4js with an AI assistant
11
+ Three files written for AI assistants ship with the package (in `node_modules/x4js/`). Give them to your assistant before asking it to write x4js code:
12
+
13
+ - [aicontext.md](./aicontext.md): compact API reference, with exact signatures
14
+ - [aiguide.md](./aiguide.md): how to write idiomatic x4js code
15
+ - [aicli.md](./aicli.md): the `x4js` command line and `x4.config.json`
16
+
17
+ With Claude Code, install the x4js plugin instead: its skill reads these files from your project by itself.
18
+
19
+ ```
20
+ claude plugin marketplace add rlibre/x4
21
+ claude plugin install x4js@rlibre
22
+ ```
11
23
 
12
24
  ---
13
25
 
package/aicli.md ADDED
@@ -0,0 +1,194 @@
1
+ # x4js command line
2
+
3
+ `x4js` creates, serves and builds x4js applications. It bundles TypeScript and styles with esbuild; there is nothing else to configure.
4
+
5
+ ```
6
+ x4js create <project> [--template <name>] [--no-install]
7
+ x4js templates
8
+ x4js dev [--config <file>] [--host <host>] [--port <port>] [--http|--https] [--open]
9
+ x4js build [--config <file>] [--debug]
10
+ x4js --help | --version
11
+ ```
12
+
13
+ ## Install
14
+
15
+ The command ships with the `x4js` package. In a project created by `x4js create` it is already a dependency, and the npm scripts use it:
16
+
17
+ ```json
18
+ {
19
+ "scripts": {
20
+ "dev": "x4js dev",
21
+ "build": "x4js build"
22
+ }
23
+ }
24
+ ```
25
+
26
+ ```
27
+ npm run dev
28
+ npm run build
29
+ ```
30
+
31
+ Without a project yet, run it through npx: `npx x4js create my-app`.
32
+
33
+ ### Which x4js runs
34
+
35
+ There can be two copies on a machine: the one in the project's `node_modules` and a global one (`npm install -g x4js`).
36
+
37
+ | You type | Copy used |
38
+ |---|---|
39
+ | `npm run dev`, `npx x4js dev` | the project's own copy |
40
+ | `x4js dev` | the global copy |
41
+
42
+ A global copy older than the project's can behave differently. `dev` and `build` print their version on the first line; check it when something looks wrong.
43
+
44
+ ```
45
+ x4js 2.3.11
46
+ mode dev
47
+ config C:\work\my-app\x4.config.json
48
+ outdir C:\work\my-app\bin
49
+ server http://127.0.0.1:8000
50
+ ```
51
+
52
+ ## Commands
53
+
54
+ ### `x4js create <project>`
55
+
56
+ Creates a new project in the folder `<project>` from a template.
57
+
58
+ | Option | Effect |
59
+ |---|---|
60
+ | `--template <name>`, `-t <name>` | Template to use. Default: `app`. |
61
+ | `--no-install` | Do not run `npm install` after creating the files. |
62
+
63
+ - The project name becomes the package name: lowercase letters, digits, `.`, `_` and `-` only.
64
+ - The target folder must be empty or not exist.
65
+ - Templates are downloaded from the `rlibre/x4-templates` repository on GitHub, so the command needs network access.
66
+ - The `x4js` dependency of the new project is set to the version line of the command that created it (for example `2.3`).
67
+
68
+ ### `x4js templates`
69
+
70
+ Lists the available templates with a one-line description of each.
71
+
72
+ ### `x4js dev`
73
+
74
+ Builds the application, serves the output folder and rebuilds on every change.
75
+
76
+ | Option | Effect |
77
+ |---|---|
78
+ | `--config <file>` | Config file to use. Default: `x4.config.json`. |
79
+ | `--host <host>` | Address to listen on. Default: `127.0.0.1`. |
80
+ | `--port <port>` | Port to listen on. Default: the first free port from 8000. |
81
+ | `--http`, `--https` | Force the protocol, whatever the config says. |
82
+ | `--open` | Open the browser once the server is ready. |
83
+
84
+ - The page reloads when a source or a copied file changes. A change that only touches CSS is applied without reloading.
85
+ - Build errors and warnings are printed in the terminal.
86
+ - Editing the config file restarts the build.
87
+ - HTTPS needs a certificate and a key, declared in `dev.tls` (see below).
88
+
89
+ ### `x4js build`
90
+
91
+ Writes the application to the output folder.
92
+
93
+ | Option | Effect |
94
+ |---|---|
95
+ | `--config <file>` | Config file to use. Default: `x4.config.json`. |
96
+ | `--debug` | Debug build: not minified, with source maps. |
97
+
98
+ The output folder is **emptied before each build**. Do not keep hand-written files in it: put them in the sources and list them in `copy`.
99
+
100
+ | | `dev` | `build --debug` | `build` |
101
+ |---|---|---|---|
102
+ | Minified | no | no | yes |
103
+ | Source maps | separate `.map` files | separate `.map` files | none |
104
+ | `DEBUG_MODE` | `true` | `true` | `false` |
105
+ | Live reload | yes | no | no |
106
+
107
+ ## `x4.config.json`
108
+
109
+ The file sits at the root of the project. Every key is optional; without the file, the defaults below apply.
110
+
111
+ ```json
112
+ {
113
+ "entryPoints": ["src/main.ts"],
114
+ "outdir": "./bin",
115
+ "copy": [
116
+ { "from": "./src/assets", "to": "assets" },
117
+ { "from": "./src/index.html", "to": "index.html" }
118
+ ]
119
+ }
120
+ ```
121
+
122
+ | Key | Default | Meaning |
123
+ |---|---|---|
124
+ | `entryPoints` | `["src/main.ts"]` | Files to bundle. `src/main.ts` produces `main.js` and `main.css`. |
125
+ | `outdir` | `"./bin"` | Output folder. It cannot be the project root. |
126
+ | `copy` | `[]` | Files or folders copied to the output after each successful build. `to` is relative to `outdir`. |
127
+ | `external` | `[]` | Module names left out of the bundle. |
128
+ | `define` | `{}` | Constants replaced at build time. Values are JavaScript expressions written as strings, for example `"\"prod\""`. |
129
+ | `dev` | | Dev server settings: `host`, `port`, `https`, `tls`. |
130
+ | `esbuild` | `{}` | Native esbuild options, applied last. They override everything above. |
131
+
132
+ Relative paths are resolved from the project root. Paths accept environment variables, written `$NAME` or `${NAME}`; an undefined variable is an error.
133
+
134
+ ### The HTML page
135
+
136
+ The command does not generate an HTML page. Write `src/index.html` and copy it with `copy`, as in the example above. It loads the bundle by its output name:
137
+
138
+ ```html
139
+ <!DOCTYPE html>
140
+ <html>
141
+ <head>
142
+ <meta charset="utf8">
143
+ <link rel="stylesheet" href="main.css">
144
+ <script src="main.js"></script>
145
+ </head>
146
+ </html>
147
+ ```
148
+
149
+ ### HTTPS in dev
150
+
151
+ ```json
152
+ {
153
+ "dev": {
154
+ "https": true,
155
+ "tls": { "cert": "./certs/dev.crt", "key": "./certs/dev.key" }
156
+ }
157
+ }
158
+ ```
159
+
160
+ ## What the build handles
161
+
162
+ | Import | Result |
163
+ |---|---|
164
+ | `.ts`, `.js` | Bundled into one script per entry point (browser, ES2020). |
165
+ | `.scss`, `.sass`, `.css` | Compiled and bundled into one stylesheet per entry point. |
166
+ | `.less` | Same, if the `less` package is installed in the project (`npm install -D less`). |
167
+ | `.svg` imported from a script | A `data:` URL string, usable as an icon. |
168
+ | `.png`, `.jpg`, `.jpeg`, `.ttf`, `.woff`, `.woff2` | Copied to `assets/` with a hashed name. |
169
+
170
+ Two constants are always defined:
171
+
172
+ | Constant | Value |
173
+ |---|---|
174
+ | `DEBUG_MODE` | `true` in `dev` and `build --debug`, `false` in `build`. |
175
+ | `VERSION_ID` | Build date as a number, `YYMMDD`. |
176
+
177
+ Declare them once in your project to use them from TypeScript:
178
+
179
+ ```ts
180
+ declare const DEBUG_MODE: boolean;
181
+ declare const VERSION_ID: number;
182
+ ```
183
+
184
+ ## Troubleshooting
185
+
186
+ | Symptom | Cause |
187
+ |---|---|
188
+ | `index.html` is missing from the output folder | It is not listed in `copy`, or the command that ran is an older global copy. Compare the version on the first line with the project's. |
189
+ | A file placed in the output folder disappeared | `x4js build` empties the folder. Move the file to the sources and add it to `copy`. |
190
+ | `Cannot find package 'esbuild'` | The dependencies of the `x4js` copy being run are not installed. Run `npm install` in the project. |
191
+ | `Config file not found` | The file given to `--config` does not exist. Without `--config`, a missing `x4.config.json` is not an error. |
192
+ | `Environment variable '…' is not defined` | A path in the config uses `$NAME` and the variable is not set. |
193
+ | `LESS support requires the "less" package` | A `.less` file is imported and `less` is not installed in the project. |
194
+ | No colors in the terminal, or unwanted ones | Set `FORCE_COLOR=1` to force them, `NO_COLOR=1` to remove them. |
package/aicontext.md CHANGED
@@ -17,6 +17,7 @@ import { ... } from 'x4js' // src/x4.ts
17
17
  **Archived repository**: https://github.com/rlibre/x4js (read-only since March 2026)
18
18
  **License**: MIT
19
19
  **Quick start**: `npx x4js create demo` (list the templates with `npx x4js templates`)
20
+ **Claude Code plugin**: `claude plugin marketplace add rlibre/x4` then `claude plugin install x4js@rlibre` (skill that reads these files from the project)
20
21
 
21
22
  ---
22
23
 
@@ -734,6 +735,7 @@ interface SpreadsheetProps extends ComponentProps {
734
735
  enterMove?: "down" | "right" | "none" // selection move after Enter in a cell editor (default "down")
735
736
  click?, dblClick?, contextMenu?, selectionChange?
736
737
  cellChange? // EvCellChange, fired before an edited value is written to the store
738
+ cellKey? // EvCellKey { key, row, col, uievent }, fired for each key before the default handling
737
739
  }
738
740
  interface SpreadsheetColumn { // GridColumn +
739
741
  editable?: boolean | ((row, col) => boolean) // inline edition (off by default), col = column index
@@ -747,7 +749,9 @@ class Spreadsheet extends Component {
747
749
  }
748
750
 
749
751
  // Inline edition: F2 / Enter / double-click / typing starts, Enter / Tab / click elsewhere validates, Escape cancels,
750
- // Delete clears, editable checkboxes toggle on click or Space. Default editor is an Input matching the column type.
752
+ // Delete clears, editable checkboxes toggle on double-click or Space (a single click only selects).
753
+ // Default editor is an Input matching the column type.
754
+ // cellKey: key hook (function keys...), not fired while a cell is edited; ev.preventDefault() skips the default handling.
751
755
  // cellChange: ev.preventDefault() refuses the value, ev.value can be replaced.
752
756
  // Select / Combobox in a cell (the store holds the id, use `formatter` to display the text):
753
757
  // { id: 2, title: "Status", width: 120, editable: true,
@@ -1392,6 +1396,7 @@ When creating a new x4js project, use these files as a base.
1392
1396
 
1393
1397
  `x4js dev` runs the dev server, `x4js build` writes `outdir` (add `--debug` for a debug build).
1394
1398
  Both read `x4.config.json` (change the file with `--config <file>`).
1399
+ Full reference of the command line (commands, options, config keys, troubleshooting): `aicli.md`.
1395
1400
 
1396
1401
  ### `x4.config.json`
1397
1402
 
package/aiguide.md CHANGED
@@ -636,6 +636,13 @@ selectLanguage("en");
636
636
  // only now build the application components
637
637
  ```
638
638
 
639
+ Type each additional language after the first one, so the compiler reports a missing text:
640
+
641
+ ```ts
642
+ const app_fr = { app: { open: "Ouvrir", close: "Fermer" } };
643
+ const app_en: typeof app_fr = { app: { open: "Open", close: "Close" } };
644
+ ```
645
+
639
646
  ---
640
647
 
641
648
  ## Final rule
package/cli/main.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { failure } from "./log.mjs";
4
+ import { failure, info } from "./log.mjs";
5
5
 
6
6
  const cliDir = path.dirname(fileURLToPath(import.meta.url));
7
7
  const packageFile = path.join(cliDir, "..", "package.json");
@@ -46,11 +46,13 @@ try {
46
46
  break;
47
47
  }
48
48
  case "dev": {
49
+ info("x4js", version);
49
50
  const { dev } = await import("./dev.mjs");
50
51
  await dev(argv);
51
52
  break;
52
53
  }
53
54
  case "build": {
55
+ info("x4js", version);
54
56
  const { build } = await import("./build.mjs");
55
57
  await build(argv);
56
58
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x4js",
3
- "version": "2.3.11",
3
+ "version": "2.3.12",
4
4
  "type": "module",
5
5
  "main": "src/x4.ts",
6
6
  "module": "src/x4.ts",
@@ -21,6 +21,7 @@
21
21
  "src",
22
22
  "bin",
23
23
  "cli",
24
+ "aicli.md",
24
25
  "aicontext.md",
25
26
  "aiguide.md",
26
27
  "README.md",
@@ -72,6 +72,18 @@ export interface EvCellChange extends ComponentEvent {
72
72
  oldValue: any;
73
73
  }
74
74
 
75
+ /**
76
+ * fired for each key pressed on the grid (not while a cell is edited), before the default handling
77
+ * call preventDefault to handle the key yourself
78
+ */
79
+
80
+ export interface EvCellKey extends ComponentEvent {
81
+ key: string; // "F2", "Enter", "a"...
82
+ row: number; // selected cell, undefined if none
83
+ col: number; // column index
84
+ uievent: KeyboardEvent; // UI event that fire this event
85
+ }
86
+
75
87
  interface CellEdit extends CellRef {
76
88
  editor: Component;
77
89
  initial?: string; // text of the editor at start
@@ -214,6 +226,7 @@ export interface SpreadsheetEvents extends ComponentEvents {
214
226
  contextMenu?: EvContextMenu;
215
227
  selectionChange?: EvSelectionChange;
216
228
  cellChange?: EvCellChange;
229
+ cellKey?: EvCellKey;
217
230
  }
218
231
 
219
232
  export interface SpreadsheetProps extends ComponentProps {
@@ -226,6 +239,7 @@ export interface SpreadsheetProps extends ComponentProps {
226
239
  enterMove?: "down" | "right" | "none";
227
240
 
228
241
  cellChange?: EventCallback<EvCellChange>;
242
+ cellKey?: EventCallback<EvCellKey>;
229
243
  click?: EventCallback<EvClick>;
230
244
  dblClick?: EventCallback<EvDblClick>;
231
245
  contextMenu?: EventCallback<EvContextMenu>;
@@ -323,7 +337,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
323
337
 
324
338
  this._columns = props.columns.map(x => x);
325
339
 
326
- this.mapPropEvents(props, "click", "dblClick", "contextMenu", "selectionChange", "cellChange");
340
+ this.mapPropEvents(props, "click", "dblClick", "contextMenu", "selectionChange", "cellChange", "cellKey");
327
341
 
328
342
  this.lock(true);
329
343
  this.setAttribute("tabindex", 0);
@@ -365,6 +379,17 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
365
379
  return;
366
380
  }
367
381
 
382
+ // the application first
383
+ const cur = this._curCell( );
384
+ const kev: EvCellKey = { key: ev.key, row: cur?.row, col: cur?.col, uievent: ev };
385
+ this.fire( "cellKey", kev );
386
+
387
+ if( kev.defaultPrevented ) {
388
+ ev.preventDefault();
389
+ ev.stopPropagation();
390
+ return;
391
+ }
392
+
368
393
  switch (ev.key) {
369
394
  case "ArrowDown": {
370
395
  this.navigate(kbNav.next);
@@ -1148,8 +1173,6 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
1148
1173
  this._clearSelection( false );
1149
1174
  this._addSelection(ref.ref,true);
1150
1175
  }
1151
-
1152
- this._toggleCell( ref.row, ref.col );
1153
1176
  }
1154
1177
  });
1155
1178
 
@@ -1170,7 +1193,8 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
1170
1193
  const dev: EvDblClick = { context: { row: ref.row, col: ref.col } };
1171
1194
  this.fire( "dblClick", dev );
1172
1195
 
1173
- if( !dev.defaultPrevented ) {
1196
+ // checkboxes are toggled, other cells are edited
1197
+ if( !dev.defaultPrevented && !this._toggleCell( ref.row, ref.col ) ) {
1174
1198
  this.editCell( ref.row, ref.col );
1175
1199
  }
1176
1200
  }
@@ -143,7 +143,13 @@ export class Router extends EventSource< RouterEvents > {
143
143
  window.addEventListener('popstate', (event) => {
144
144
  const url = this._getLocation( );
145
145
  const found = this._find(url);
146
-
146
+
147
+ if( found.handlers.length==0 ) {
148
+ console.log( 'route not found: '+url );
149
+ this.fire( "error", {code: 404, message: "route not found" } );
150
+ return;
151
+ }
152
+
147
153
  found.handlers.forEach(h => {
148
154
  h(found.params,url);
149
155
  });
@@ -173,11 +179,17 @@ export class Router extends EventSource< RouterEvents > {
173
179
 
174
180
 
175
181
  init() {
176
- this.navigate( this._getLocation() );
182
+ // the page is already in the history: replace its entry instead of adding one
183
+ this.navigate( this._getLocation(), true, true );
177
184
  }
178
185
 
179
186
  private _getLocation( ) {
180
- return this.m_useHash ? '/'+document.location.hash.substring(1) : document.location.pathname;
187
+ if( !this.m_useHash ) {
188
+ return document.location.pathname;
189
+ }
190
+
191
+ // "#home" and "#/home" are the same route
192
+ return '/'+document.location.hash.substring(1).replace( /^\/+/, '' );
181
193
  }
182
194
 
183
195
  /**