diff --git a/ivette/.eslintrc.js b/ivette/.eslintrc.js
index 163e36aee57deb7cfda15a74345bb71c0f2eb006..ebdbbb656e9a0007b96ff7f378c22fd3c3401f78 100644
--- a/ivette/.eslintrc.js
+++ b/ivette/.eslintrc.js
@@ -67,6 +67,8 @@ module.exports = {
     "no-return-assign": ["error", "except-parens" ],
     // Allow single line expressions in react
     "react/jsx-one-expression-per-line": "off",
+    // Allow property spreading since with aim at using TSC
+    "react/jsx-props-no-spreading": "off",
     // Allow all sorts of linebreaking for operators
     "operator-linebreak": "off",
     // Force curly brackets on newline if some item is
diff --git a/ivette/package.json b/ivette/package.json
index 847f6c4cd4a65f95f1356a03c94d6e0be4f2702e..d77d420e12005ff11011b27dd7bca04d6531db44 100644
--- a/ivette/package.json
+++ b/ivette/package.json
@@ -26,6 +26,7 @@
     "@types/node": "12.12.21",
     "@types/react": "^16.9.17",
     "@types/react-dom": "^16.9.6",
+    "@types/react-virtualized": "^9.21.10",
     "@typescript-eslint/eslint-plugin": "^2.28.0",
     "@typescript-eslint/parser": "^2.28.0",
     "babel-loader": "^8.0.6",
@@ -43,6 +44,7 @@
     "eslint-plugin-react-hooks": "^3.0.0",
     "html-loader": "1.0.0-alpha.0",
     "jsdoc": "^3.6.3",
+    "react-fast-compare": "^3.2.0",
     "react-hot-loader": "^4.12.20",
     "serve": "^11.3.0",
     "typedoc": "^0.17.6",
diff --git a/ivette/src/dome/src/main/dome.js b/ivette/src/dome/src/main/dome.js
index c1f1452eb93ee84409a60bc9e49bb4f25a1f868a..1385eb6bc5e50f273d46a85201ae79d68e74e093 100644
--- a/ivette/src/dome/src/main/dome.js
+++ b/ivette/src/dome/src/main/dome.js
@@ -196,6 +196,7 @@ function navigateURL( event , url ) {
 // --------------------------------------------------------------------------
 
 const windowsHandle = {} ; // Prevent live windows to be garbage collected
+const windowsReload = {} ; // Reloaded windows
 
 function createBrowserWindow( config, isMain=true )
 {
@@ -226,7 +227,8 @@ function createBrowserWindow( config, isMain=true )
   }
 
   const theWindow = new BrowserWindow( options );
-
+  const wid = theWindow.id;
+  
   // Load the index.html of the app.
   if (DEVEL || LOCAL)
     process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true';
@@ -249,7 +251,14 @@ function createBrowserWindow( config, isMain=true )
   // URL Navigation
   theWindow.webContents.on('will-navigate', navigateURL );
   theWindow.webContents.on('did-navigate-in-page', navigateURL );
-  theWindow.webContents.on('did-finish-load', () => broadcast('dome.ipc.reload'));
+  theWindow.webContents.on('did-finish-load', () => {
+    const isLoaded = windowsReload[wid];
+    if (!isLoaded) {
+      windowsReload[wid] = true;
+    } else {
+      broadcast('dome.ipc.reload');
+    }
+  });
 
   // Emitted when the window want's to close.
   theWindow.on('close', (evt) => {
@@ -270,7 +279,6 @@ function createBrowserWindow( config, isMain=true )
   }
 
   // Keep the window reference to prevent destruction
-  const wid = theWindow.id ;
   windowsHandle[ wid ] = theWindow ;
 
   // Emitted when the window is closed.
diff --git a/ivette/src/dome/src/renderer/data/compare.ts b/ivette/src/dome/src/renderer/data/compare.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6dcbec1d018875f4d3eceff7f1eafb345d04b700
--- /dev/null
+++ b/ivette/src/dome/src/renderer/data/compare.ts
@@ -0,0 +1,371 @@
+// --------------------------------------------------------------------------
+// --- Comparison Utilities
+// --------------------------------------------------------------------------
+
+/**
+   Data comparisons.
+   @packageDocumentation
+   @module dome/data/compare
+*/
+
+/**
+   Interface for comparison functions.
+   These function shall fullfill the following contract:
+   - `compare(x,y) == 0` shall be an equivalence relation
+     (reflexive, symmetric, transitive)
+   - `compare(x,y) <= 0` shall be a complete order
+     (reflexive, antisymetric, transitive)
+   - `compare(x,y) < 0` shall be a complete strict order
+     (anti-reflexive, asymetric, transitive)
+*/
+export interface Order<A> {
+  (x: A, y: A): number;
+}
+
+export function equal(_x: any, _y: any): 0 { return 0; }
+
+export type bignum = bigint | number;
+
+/** Non-NaN numbers and big-ints */
+export function isBigNum(x: any): x is bignum {
+  return typeof (x) === 'bigint' || (typeof (x) === 'number' && !Number.isNaN(x));
+}
+
+/**
+   Primitive comparison.
+   Can only compare arguments that have
+   comparable primitive type.
+
+   This includes symbols, boolean, non-NaN numbers, bigints and strings.
+   Numbers and big-ints can also be compared with each others.
+*/
+export function primitive(x: symbol, y: symbol): number;
+export function primitive(x: boolean, y: boolean): number;
+export function primitive(x: bignum, y: bignum): number;
+export function primitive(x: string, y: string): number;
+export function primitive(x: any, y: any) {
+  if (x < y) return -1;
+  if (x > y) return 1;
+  return 0;
+}
+
+/**
+   Primitive comparison for numbers (NaN included).
+ */
+export function float(x: number, y: number) {
+  const nx = Number.isNaN(x);
+  const ny = Number.isNaN(y);
+  if (nx && ny) return 0;
+  if (nx && !ny) return -1;
+  if (!nx && ny) return 1;
+  if (x < y) return -1;
+  if (x > y) return 1;
+  return 0;
+}
+
+/**
+   Alphabetic comparison for strings.
+   Handles case differently than `byString` comparison.
+*/
+export function alpha(x: string, y: string) {
+  const cmp = primitive(x.toLowerCase(), y.toLowerCase());
+  return cmp != 0 ? cmp : primitive(x, y);
+}
+
+/** Combine comparison orders in sequence. */
+export function sequence<A>(...orders: (Order<A> | undefined)[]): Order<A> {
+  return (x: A, y: A) => {
+    if (x === y) return 0;
+    for (const order of orders) {
+      if (order) {
+        const cmp = order(x, y);
+        if (cmp != 0) return cmp;
+      }
+    }
+    return 0;
+  };
+}
+
+/** Compare optional values. Undefined values come first. */
+export function option<A>(order: Order<A>): Order<undefined | A> {
+  return (x?: A, y?: A) => {
+    if (x == undefined && y == undefined) return 0;
+    if (x == undefined) return -1;
+    if (y == undefined) return 1;
+    return order(x, y);
+  };
+}
+
+/** Compare optional values. Undefined values come last. */
+export function defined<A>(order: Order<A>): Order<undefined | A> {
+  return (x?: A, y?: A) => {
+    if (x == undefined && y == undefined) return 0;
+    if (x == undefined) return 1;
+    if (y == undefined) return -1;
+    return order(x, y);
+  };
+}
+
+/** Lexicographic comparison of array elements. */
+export function array<A>(order: Order<A>): Order<A[]> {
+  return (x: A[], y: A[]) => {
+    if (x === y) return 0;
+    const p = x.length;
+    const q = y.length;
+    const m = p < q ? p : q;
+    for (let k = 0; k < m; k++) {
+      const cmp = order(x[k], y[k]);
+      if (cmp != 0) return cmp;
+    }
+    return p - q;
+  };
+}
+
+/** Order string enumeration constants.
+    `enums(v1,...,vN)` will order constant following the order of arguments.
+    Non-listed constants appear at the end, or at the rank specified by `'*'`. */
+export function byRank(...args: string[]): Order<string> {
+  const ranks: { [index: string]: number } = {};
+  args.forEach((C, k) => ranks[C] = k);
+  const wildcard = ranks['*'] ?? ranks.length;
+  return (x: string, y: string) => {
+    if (x === y) return 0;
+    const rx = ranks[x] ?? wildcard;
+    const ry = ranks[y] ?? wildcard;
+    if (rx == wildcard && ry == wildcard)
+      return primitive(x, y);
+    else
+      return rx - ry;
+  };
+}
+
+/** Direct or reverse direction. */
+export function direction<A>(order: Order<A>, reverse = false): Order<A> {
+  return (x, y) => (x === y ? 0 : reverse ? order(y, x) : order(x, y));
+}
+
+/** By projection. */
+export function lift<A, B>(fn: (x: A) => B, order: Order<B>): Order<A> {
+  return (x: A, y: A) => (x === y ? 0 : order(fn(x), fn(y)));
+}
+
+/** Return own property names of its object argument. */
+export function getKeys<T>(a: T): (keyof T)[] {
+  return Object.getOwnPropertyNames(a) as (keyof T)[];
+}
+
+/**
+   Maps each field of `A` to some _optional_ comparison of the associated type.
+   Hence, `ByFields<{…, f: T, …}>` is `{…, f?: Order<T>, …}`.
+   See [[fields]] comparison function.
+ */
+export type ByFields<A> = {
+  [P in keyof A]?: Order<A[P]>;
+}
+
+/**
+   Maps each field of `A` to some comparison of the associated type.
+   Hence, `ByAllFields<{…, f: T, …}>` is `{…, f: Order<T>, …}`.
+   See [[fieldsComplete]] comparison function.
+*/
+export type ByAllFields<A> = {
+  [P in keyof A]: Order<A[P]>;
+}
+
+/** Object comparison by (some) fields.
+
+    Compare objects field by field, using the comparison orders provided by the
+    `order` argument. Order of field comparison is taken from the `order`
+    argument, not from the compared values.
+
+    You may not compare _all_ fields of the compared values.  For optional
+    fields, you shall provide a comparison function compatible with type
+    `undefined`.
+
+    It might be difficult for Typescript to typecheck `byFields(…)` expressions
+    when dealing with optional types. In such cases, you shall use `byFields<A>(…)`
+    and explicitly mention the type of compared values.
+
+    Example:
+
+        type foo = { id: number, name?: string, descr?: string }
+        const compare = fields<foo>({ id: number, name: option(alpha) });
+
+*/
+export function byFields<A>(order: ByFields<A>): Order<A> {
+  return (x: A, y: A) => {
+    if (x === y) return 0;
+    for (const fd of getKeys(order)) {
+      const byFd = order[fd];
+      if (byFd !== undefined) {
+        const cmp = byFd(x[fd], y[fd]);
+        if (cmp != 0) return cmp;
+      }
+    }
+    return 0;
+  };
+}
+
+/** Complete object comparison.
+    This is similar to `byFields()` comparison, but an ordering function must be
+    provided for _any_ field (optional or not) of the compared values.
+*/
+export function byAllFields<A>(order: ByAllFields<A>): Order<A> {
+  return (x: A, y: A) => {
+    if (x === y) return 0;
+    for (const fd of getKeys<ByFields<A>>(order)) {
+      const byFd = order[fd];
+      const cmp = byFd(x[fd], y[fd]);
+      if (cmp != 0) return cmp;
+    }
+    return 0;
+  };
+}
+
+/** Pair comparison. */
+export function pair<A, B>(ordA: Order<A>, ordB: Order<B>): Order<[A, B]> {
+  return (u, v) => {
+    if (u === v) return 0;
+    const [x1, y1] = u;
+    const [x2, y2] = v;
+    const cmp = ordA(x1, x2);
+    return cmp != 0 ? cmp : ordB(y1, y2);
+  };
+}
+
+/** Triple comparison. */
+export function triple<A, B, C>(
+  ordA: Order<A>,
+  ordB: Order<B>,
+  ordC: Order<C>,
+): Order<[A, B, C]> {
+  return (u, v) => {
+    if (u === v) return 0;
+    const [x1, y1, z1] = u;
+    const [x2, y2, z2] = v;
+    const cmp1 = ordA(x1, x2);
+    if (cmp1 != 0) return cmp1;
+    const cmp2 = ordB(y1, y2);
+    if (cmp2 != 0) return cmp2;
+    return ordC(z1, z2);
+  };
+}
+
+/** 4-Tuple comparison. */
+export function tuple4<A, B, C, D>(
+  ordA: Order<A>,
+  ordB: Order<B>,
+  ordC: Order<C>,
+  ordD: Order<D>,
+): Order<[A, B, C, D]> {
+  return (u, v) => {
+    if (u === v) return 0;
+    const [x1, y1, z1, t1] = u;
+    const [x2, y2, z2, t2] = v;
+    const cmp1 = ordA(x1, x2);
+    if (cmp1 != 0) return cmp1;
+    const cmp2 = ordB(y1, y2);
+    if (cmp2 != 0) return cmp2;
+    const cmp3 = ordC(z1, z2);
+    if (cmp3 != 0) return cmp3;
+    return ordD(t1, t2);
+  };
+}
+
+/** 5-Tuple comparison. */
+export function tuple5<A, B, C, D, E>(
+  ordA: Order<A>,
+  ordB: Order<B>,
+  ordC: Order<C>,
+  ordD: Order<D>,
+  ordE: Order<E>,
+): Order<[A, B, C, D, E]> {
+  return (u, v) => {
+    if (u === v) return 0;
+    const [x1, y1, z1, t1, w1] = u;
+    const [x2, y2, z2, t2, w2] = v;
+    const cmp1 = ordA(x1, x2);
+    if (cmp1 != 0) return cmp1;
+    const cmp2 = ordB(y1, y2);
+    if (cmp2 != 0) return cmp2;
+    const cmp3 = ordC(z1, z2);
+    if (cmp3 != 0) return cmp3;
+    const cmp4 = ordD(t1, t2);
+    if (cmp4 != 0) return cmp4;
+    return ordE(w1, w2);
+  };
+}
+
+// --------------------------------------------------------------------------
+// --- Structural Comparison
+// --------------------------------------------------------------------------
+
+/** @internal */
+enum RANK { UNDEFINED, BOOLEAN, SYMBOL, NAN, BIGNUM, STRING, ARRAY, OBJECT, FUNCTION };
+
+/** @internal */
+function rank(x: any): RANK {
+  let t = typeof x;
+  switch (t) {
+    case 'undefined': return RANK.UNDEFINED;
+    case 'boolean': return RANK.BOOLEAN;
+    case 'symbol': return RANK.SYMBOL;
+    case 'number':
+      return Number.isNaN(x) ? RANK.NAN : RANK.BIGNUM;
+    case 'bigint':
+      return RANK.BIGNUM;
+    case 'string': return RANK.STRING;
+    case 'object': return Array.isArray(x) ? RANK.ARRAY : RANK.OBJECT;
+    case 'function': return RANK.FUNCTION;
+  }
+}
+
+/**
+   Universal structural comparison.
+   Values are ordered by _rank_, each being associated with some type of values:
+   1. undefined values;
+   2. booleans;
+   3. symbols;
+   4. NaN numbers;
+   5. non-NaN numbers and bigints;
+   6. arrays;
+   7. objects;
+   8. functions;
+
+   For values of same primitive type, primitive ordering is performed.
+
+   For array values, lexicographic ordering is performed.
+
+   For object values, lexicographic ordering is performed over their properties:
+   properties are ordered by name, and recursive structural ordering is performed
+   on property values.
+
+   All functions are compared equal.
+ */
+export function structural(x: any, y: any): number {
+  if (x === y) return 0;
+  if (typeof x === 'symbol' && typeof y === 'symbol') return primitive(x, y);
+  if (typeof x === 'boolean' && typeof y === 'boolean') return primitive(x, y);
+  if (typeof x === 'string' && typeof y === 'string') return primitive(x, y);
+  if (isBigNum(x) && isBigNum(y)) return primitive(x, y);
+  if (Array.isArray(x) && Array.isArray(y)) return array(structural)(x, y);
+  if (typeof x === 'object' && typeof y === 'object') {
+    const fs = Object.getOwnPropertyNames(x).sort();
+    const gs = Object.getOwnPropertyNames(y).sort();
+    const p = fs.length;
+    const q = gs.length;
+    for (let i = 0, j = 0; i < p && j < q;) {
+      let a = undefined, b = undefined;
+      const f = fs[i];
+      const g = gs[j];
+      if (f <= g) { a = x[f]; i++; }
+      if (g <= f) { b = y[g]; j++; }
+      const cmp = structural(a, b);
+      if (cmp != 0) return cmp;
+    }
+    return p - q;
+  }
+  return rank(x) - rank(y);
+};
+
+// --------------------------------------------------------------------------
diff --git a/ivette/src/dome/src/renderer/data.js b/ivette/src/dome/src/renderer/data/library.js
similarity index 99%
rename from ivette/src/dome/src/renderer/data.js
rename to ivette/src/dome/src/renderer/data/library.js
index 2105a9bfafdc20127753ecc59635233daf6c1b6f..e175217eceddafcbbd811d0a53d7ac15e6e7c637 100644
--- a/ivette/src/dome/src/renderer/data.js
+++ b/ivette/src/dome/src/renderer/data/library.js
@@ -4,7 +4,7 @@
 
 /**
    @packageDocumentation
-   @module dome/data
+   @module dome/data/library
    @description
    This module allows to integrate data definitions within React elements.
 
diff --git a/ivette/src/dome/src/renderer/dome.js b/ivette/src/dome/src/renderer/dome.js
index e3986b692c9baf6c17c16a3ffe96029ab76bb1e4..6fb25c3702fd9c8e80c8ae9939650783ee9abc8a 100644
--- a/ivette/src/dome/src/renderer/dome.js
+++ b/ivette/src/dome/src/renderer/dome.js
@@ -77,6 +77,10 @@ export function off(evt,job) { emitter.off(evt,job); }
 /** Same as `emitter.emit` */
 export function emit(evt,...args) { emitter.emit(evt,...args); }
 
+{
+  emitter.setMaxListeners(250);
+}
+
 // --------------------------------------------------------------------------
 // --- Application Events
 // --------------------------------------------------------------------------
@@ -476,7 +480,7 @@ ipcRenderer.on('dome.ipc.settings.update',(sender,patches) => {
 
 /**
     @summary Get value from local window (persistent) settings.
-    @param {string} key User's Setting Key (`'dome.*'` are reserved keys)
+    @param {string} [key] -  User's Setting Key (`'dome.*'` are reserved keys)
     @param {any} [defaultValue] - default value if the key is not present
     @return {any} associated value of object or `undefined`.
     @description
diff --git a/ivette/src/dome/src/renderer/table/arrays.js b/ivette/src/dome/src/renderer/table/arrays.js
deleted file mode 100644
index 19f0fd189fc614060c975186da2660c2ba2c47f1..0000000000000000000000000000000000000000
--- a/ivette/src/dome/src/renderer/table/arrays.js
+++ /dev/null
@@ -1,524 +0,0 @@
-// --------------------------------------------------------------------------
-// --- Table Models
-// --------------------------------------------------------------------------
-
-/**
-   @packageDocumentation
-   @module dome/table/arrays
-*/
-
-import _ from 'lodash' ;
-import React from 'react' ;
-import { Model, ASC, DESC } from 'dome/table/models' ;
-
-// --------------------------------------------------------------------------
-// --- Ordering
-// --------------------------------------------------------------------------
-
-// Compute the value of an item
-const getValueWith =
-      ( item , value ) =>
-      ( item === undefined ? undefined :
-        typeof(value) === 'function' ? value(item) :
-        item[value] );
-
-// Compute the primary ordering function
-const comparisonWith = (sorting) => {
-  switch(typeof(sorting)) {
-
-  case 'function':
-    return sorting ;
-
-  case 'string':
-    return (a,b) => {
-      if ( a === b ) return 0 ;
-      if ( a === undefined ) return 1 ;
-      if ( b === undefined ) return -1 ;
-      const va = a[sorting];
-      const vb = b[sorting];
-      if ( va === vb ) return 0;
-      if ( va === undefined ) return 1 ;
-      if ( vb === undefined ) return -1 ;
-      if ( va < vb ) return -1;
-      if ( va > vb ) return 1;
-      return 0;
-    };
-
-  case 'object':
-    const { sortBy, sortDirection=ASC } = sorting ;
-    return (a,b) => {
-      if ( a === b ) return 0 ;
-      if ( a === undefined ) return 1 ;
-      if ( b === undefined ) return -1 ;
-      const isFun = typeof(sortBy)==='function' ;
-      const va = isFun ? sortBy(a) : a[sortBy] ;
-      const vb = isFun ? sortBy(b) : b[sortBy] ;
-      if ( va === vb ) return 0;
-      if ( va === undefined ) return 1 ;
-      if ( vb === undefined ) return -1 ;
-      switch(sortDirection) {
-      case ASC:
-        if (va < vb) return -1;
-        if (va > vb) return 1;
-        break;
-      case DESC:
-        if (va < vb) return 1;
-        if (va > vb) return -1;
-        break;
-      }
-      return 0;
-    };
-
-  default:
-    return () => 0;
-
-  }
-};
-
-// Make a chainable order
-const chainableOrder = ( order ) => {
-
-  const compare = (a,b) => {
-    for (var k = 0; k < order.length ; k++) {
-      const cmp = (order[k])(a,b);
-      if (cmp !==0 ) return cmp;
-    }
-    return 0;
-  };
-
-  compare.thenWith = (sorting) =>
-    sorting ?
-    chainableOrder( order.slice().push(comparisonWith(sorting)) )
-    : compare ;
-
-  return compare ;
-};
-
-/**
-   @summary Comparison helper.
-   @method
-   @param {any} sorting - the sorting properties
-   @return {function} the corresponding comparison function
-   @description
-
-This function is a helper for comparing items, by comparing
-values extracted from them with chaining. It returns
-a comparison function that you can use, for instance,
-in `Array.sort` fort sorting arrays.
-
-##### Comparison
-
-The comparison order is defined according the `sorting` parameter
-provided to the helper:
-
-If `sorting` is a function, it is used as the primary comparison function.
-When items compare equal, chained comparisons are used to refine the
-ordering (see Chaining below).
-
-If `sorting` is a property name (a string), items `a` and `b` are ordered
-by comparing their respective values `a[sorting]` and `b[sorting]` for
-this property.
-
-If `sorting` as an object, it shall provides `{ sortBy, sortDirection }`
-properties and the comparison function is defined as follows:
-- `sortBy` can be a function or a property name.
-If a function is given, items `a` and `b` are ordered by comparing
-values `sortBy(a)` and `sortBy(b)`. Otherwize, they are ordered by
-comparing `a[sortBy]` and `b[sortBy]`.
-- `sortDirection` can be either `ASC` for normal comparison or `DESC`
-for reversing the comparison.
-If no direction is given (or any other value) `ASC` is assumed.
-
-When computing comparison, `undefined` values are _always_ rejected to the
-end of the ordering, whatever the specified direction.
-
-##### Chaining
-
-The returned comparison function can be chained with a secondary
-comparison function, which will be used when two items compare equal.
-
-You can chain as many comparison functions you want by using
-`.thenWith(sorting)` like in the example below. Each call to `.thenWith` returns
-a different comparison function that can be safely forked with subsequent calls
-to `.thenWith` as illustrated in the second example.
-
-Remark than `compareWith` and `.thenWith` also accept undefined or null values, which
-are considered neutral.
-
-@example
-// Chaining Comparison
-items.sort(
-  compareWith({sortBy:'name',sortDirection: ASC})
-    .thenWith( (a,b) => a.priority - b.priority )
-    .thenWith({sortBy:'age',sortDirection: DESC})
-);
-
-@example
-// Forking Comparison
-const primary = compareWith( (a,b) => a.priority - b.prioroty );
-const byName = primary.thenWith('name');
-const byAge = primary.thenWith('age');
-
-*/
-export const compareWith =
-  ( sorting ) =>
-  ( sorting ? chainableOrder([ comparisonWith(sorting) ]) : () => 0 );
-
-// --------------------------------------------------------------------------
-// --- Comparison Ring
-// --------------------------------------------------------------------------
-
-/**
-   @class
-   @summary Helper for column comparison.
-   @description
-A comparison ring can be used to implement column ordering, where each
-column selection _refines_ the previous ordering.
-
-Hence, the ring holds the current comparison order and it is well suited
-for being used in conjunction with [[Table]]
-components.
-
-A comparison specification can be a property name or a function.
-See [[compareWith]] for more details.
-By default, the ring uses the column identifier as a property name for comparing.
-
-Initially, the ordering is _natural_.
-*/
-export class ComparisonRing {
-
-  /**
-     @param {id|function} [natural] - default (natural) order
-  */
-
-  constructor(natural) {
-    this.columns = {} ;
-    this.natural = natural && comparisonWith(natural) ;
-    this.compare = this.compare.bind(this);
-    this.ring = [] ;
-    this.sort = undefined ;
-  }
-
-  /**
-     @summary Current order comparison.
-     @description
-     Returns the comparison function corresponding to the current order.
-     The comparison function is _chainable_, see
-     [[compareWith]] for more details.
-  */
-  compareWith()
-  {
-    if (!this.sort) {
-      const ordering = this.ring.map(
-        ({sortBy,sortDirection}) =>
-          comparisonWith({
-            sortBy: this.columns[sortBy] || sortBy ,
-            sortDirection
-          })
-      );
-      if (this.natural) ordering.push(this.natural);
-      this.sort = chainableOrder(ordering);
-    }
-    return this.sort;
-  }
-
-  /**
-     @summary Refine current order comparison.
-     @description
-     Short cut for `this.compareWith().thenWith(sorting)`
-  */
-  thenWith(sorting) {
-    return this.compareWith().thenWith(sorting);
-  }
-
-  /**
-     @summary Get value for ordering a column.
-     @param {any} item - the item to compare with
-     @param {string} column - the column identifier
-     @return {any} item's value for the column
-     Returns the value used to order an item within a given column.
-  */
-  getValue(item,column) {
-    return getValueWith(item,this.columns[column] || column);
-  }
-
-  /**
-     @summary Set value to order a column.
-     @param {string} column - column identifier
-     @param {string|function} [value] - value accessor (defaults to `column`)
-     @description
-     Set the sorting specification (property name or function) for the given column.
-  */
-  setValue( column , value ) {
-    this.columns[column] = value ;
-  }
-
-  /** Sets natural ordering (property name or value accessor) */
-  setNatural( natural ) {
-    this.natural = natural ;
-  }
-
-  /** @summary Compare two items with respect to the current ordering.
-      @description
-      You can use `this.compare` as a closure to the current comparison function
-      (no need for `this.compare.bind(this)`).
-   */
-  compare(a,b) {
-    return this.order()(a,b);
-  }
-
-  /** @summary Return current ordering.
-      @return {object} ordering specification
-      @description
-      Return the last `{ sortBy, sortDirection }` ordering.
-      Shall be used for the Table view. */
-  getOrdering() {
-    return this.ring[0] ;
-  }
-
-  /**
-     @summary Specify current ordering.
-     @param {object} sorting - the new ordering
-     @description
-     Use the specified `{ sortBy, sortDirection }` ordering, and refine it with
-     the previous one.
-
-     If `sorting` is undefined, reset the ring to the natural ordering.
-  */
-  setOrdering( sorting ) {
-    if (sorting) {
-      const key = sorting.sortBy ;
-      this.ring = this.ring.filter( (s) => s.sortBy !== key );
-      this.ring.unshift( sorting );
-    } else {
-      this.ring = [] ;
-    }
-    this.sort = undefined ;
-  }
-
-}
-
-// --------------------------------------------------------------------------
-// --- Unsorted Model
-// --------------------------------------------------------------------------
-
-/**
-   @summary A table Model for unsorted datasets.
-   @description
-
-   This class implements a simple [[Model]]
-   where item's are identified by their index. Such a model is not adapted to
-   re-ordering and filtering, because table views will have no way to synchronize
-   the selected index before and after re-ordering, hence the name.
-
-*/
-
-export class UnsortedModel extends Model {
-
-  /**
-      @param {number} [count] - the initial size (default `0`)
-   */
-  constructor(count=0) {
-    super();
-    this.count = count < 0 ? 0 : count ;
-  }
-
-  /**
-     @summary Set the number of items.
-     @param {number} n - the number of items
-     @param {boolean} [reload] - force reloading (false by default)
-     @description
-     Triggers a reload if the item count has changed,
-     unless you force it.
-  */
-  setItemCount(n,reload=false) {
-    if (n<0) n=0;
-    if ( reload || n != this.count ) {
-      this.count = n ;
-      this.reload();
-    }
-  }
-
-  getItemCount() { return this.count; }
-
-  /** Identity, or `undefined` when out of range. */
-  getItemAt(k) {
-    return 0 <= k && k < this.count ? k : undefined ;
-  }
-
-  /** Identity, or `-1` when out of range. */
-  getIndexOf(k) {
-    return 0 <= k && k < this.count ? k : -1 ;
-  }
-
-}
-
-// --------------------------------------------------------------------------
-// --- Array Model
-// --------------------------------------------------------------------------
-
-/**
-   @summary A table Model based on array.
-   @extends Model
-   @description
-
-   This class implements a simple [[Model]]
-   implementation where item's are stored in an array. The model supports built-in
-   ordering thanks with a
-   [[ComparisonRing]] with additional filtering capabilities.
-
-   The model keep items in sync with their ordered & filtered index by
-   injecting an `index` property in them each time the collection is re-ordered.
-   You shall not use `index` property for your own needs.
-
-   Item objects can be modified in place, but you shall call `model.updateItem(item)`
-   or `model.reload()` to re-renderer the associated (visible) cells.
- */
-export class ArrayModel extends Model {
-
-  /** Initially empty model */
-  constructor() {
-    super();
-    this.ring = new ComparisonRing('index'); // Used for stable sorting
-    this.data = []; // Array of item elements
-  }
-
-  /** Remove all items (and reload) */
-  clear() {
-    this.data = [] ;
-    this.reload();
-  }
-
-  /** Add one or more items (and reload) */
-  add( ...items ) {
-    this.data.push(...items);
-    this.reload();
-  }
-
-  /**
-     @summary Model items array.
-     @description
-Returns the internal item array holding _all_ the items in the model.
-
-This array is _not_ sortered and filtered. You can obtain the current index of an item in
-table views by accessing its `item.index` property, which is `undefined` if the item has been
-filtered out.
-
-If you modify the internal item array, don't forget to call `reload()` after modifications
-in order to keep views in sync.
-   */
-  getData() { return this.data; }
-
-  /** Replace the entire collection of items */
-  setData(data) {
-    this.data = data || [] ;
-    this.reload();
-  }
-
-  _order() {
-    if (!this.order) {
-      const filter = this.filtering ;
-      const compare = this.ring.compareWith();
-      const ordered = this.data.filter((item,index) => {
-        const ok = filter ? filter(item) : true ;
-        // Index inside initial collection is used for stable sort
-        item.index = ok ? index : undefined ;
-        return ok ;
-      }).sort(compare);
-      // Now set index in filtered & sorted collection
-      ordered.forEach((item,index) => item.index = index);
-      this.order = ordered ;
-    }
-    return this.order ;
-  }
-
-  /** Return a _copy_ of the visible items. */
-  getItems() { return this._order().slice(); }
-
-  // MODEL Interface
-  getItemCount() { return this._order().length; }
-
-  // MODEL Interface
-  getItemAt(index) { return this._order()[index] ; }
-
-  // MODEL Interface
-  getIndexOf(item) { return item && item.index ; }
-
-  /** @summary Current filtering function.
-      @return {function} `undefined` means no filtering
-  */
-  getFiltering() { return this.filtering ; }
-
-  /** @summary Set the filtering function.
-      @param {function} [filter] - The filter function
-      @description
-      The filtering function is used to filter out items to be displayed.
-      It is invoked as `filter(item)` and shall return a truthly value when `item`
-      must be displayed.
-  */
-  setFiltering(filter) {
-    this.filtering = filter ;
-    this.reload();
-  }
-
-  // MODEL Interface
-  getOrdering() { return this.ring.getOrdering(); }
-
-  // MODEL Interface
-  setOrdering(order) {
-    if (order === undefined || order.sortBy !== 'index')
-    {
-      this.ring.setOrdering(order);
-      this.reload();
-    }
-  }
-
-  // MODEL Interface
-  getValue( item , column ) {
-    return this.ring.getValue( item , column );
-  }
-
-  /** Set the value-getter for the given column */
-  setValue( column , value ) {
-    this.ring.setValue(column,value);
-    this.reload();
-  }
-
-  // MODEL Interface
-  reload() {
-    this.order = undefined ;
-    super.reload();
-  }
-}
-
-// --------------------------------------------------------------------------
-// --- Model Hook
-// --------------------------------------------------------------------------
-
-/**
-   @summary Uses a new array model (Custom React Hook).
-   @param {Collection} [items] - the array items
-   @description
-   This hook is a convenient way to have a local array model with full featured
-   sorting and filtering fonctionnalities, which is automatically updated
-   with the provided items.
-
-   The array model is created once and updated at each render.
-   Items can be specified with a lodash collection.
-
-   @example // Array Model
-   const MyView = () => {
-      Dome.useUpdate( MyUpdateEvent );
-      const model = useArrayModel(getMyItems());
-      return (<Table model={model} … >…</Table>) ;
-   };
-
-*/
-export function useArrayModel( items )
-{
-  const model = React.useMemo( () => new ArrayModel() , [] );
-  model.setData( _.toArray(items) );
-  return model;
-}
-
-// --------------------------------------------------------------------------
diff --git a/ivette/src/dome/src/renderer/table/arrays.ts b/ivette/src/dome/src/renderer/table/arrays.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cade711487035d5799102b860ddec55d7c29dc77
--- /dev/null
+++ b/ivette/src/dome/src/renderer/table/arrays.ts
@@ -0,0 +1,448 @@
+// --------------------------------------------------------------------------
+// --- Array Models
+// --------------------------------------------------------------------------
+
+/**
+   @packageDocumentation
+   @module dome/table/arrays
+*/
+
+import * as Compare from 'dome/data/compare';
+import type { ByFields, Order } from 'dome/data/compare';
+import {
+  SortingInfo, Sorting, Filter, Filtering, Model, Collection, forEach
+} from './models';
+
+// --------------------------------------------------------------------------
+// --- Sorting Utilities
+// --------------------------------------------------------------------------
+
+export type ByColumns<Row> = { [dataKey: string]: Compare.Order<Row> };
+
+interface PACK<Key, Row> {
+  index: number | undefined;
+  key: Key;
+  row: Row;
+};
+
+type SORT<K, R> = Order<PACK<K, R>>;
+
+function orderBy<K, R>(
+  columns: ByColumns<R>,
+  ord: SortingInfo,
+): SORT<K, R> {
+  const dataKey = ord.sortBy;
+  const byData = columns[dataKey] ?? Compare.equal;
+  const rv = ord.sortDirection === 'DESC';
+  type D = PACK<K, R>;
+  const byEntry = (x: D, y: D) => byData(x.row, y.row);
+  const byIndex = (x: D, y: D) => (x.index ?? 0) - (y.index ?? 0);
+  return Compare.direction(Compare.sequence(byEntry, byIndex), rv);
+}
+
+function orderByRing<K, R>(
+  natural: undefined | Order<R>,
+  columns: undefined | ByColumns<R>,
+  ring: SortingInfo[],
+): SORT<K, R> {
+  type D = PACK<K, R>;
+  const byRing = columns ? ring.map((ord) => orderBy(columns, ord)) : [];
+  const byData = natural ? ((x: D, y: D) => natural(x.row, y.row)) : undefined;
+  return Compare.sequence(...byRing, byData);
+}
+
+// --------------------------------------------------------------------------
+// --- Filtering Utilities
+// --------------------------------------------------------------------------
+
+type INDEX<K, R> = Map<K, PACK<K, R>>;
+type TABLE<K, R> = PACK<K, R>[];
+
+// --------------------------------------------------------------------------
+// --- Array Model
+// --------------------------------------------------------------------------
+
+export class MapModel<Key, Row>
+  extends Model<Key, Row>
+  implements Sorting, Filtering<Key, Row>
+{
+
+  // Hold raw data (unsorted, unfiltered)
+  private index: INDEX<Key, Row> = new Map();
+
+  // Hold filtered & sorted data (computed on demand)
+  private table?: TABLE<Key, Row>;
+
+  // Filtering function
+  private filter?: Filter<Key, Row>;
+
+  // Natural ordering (if any)
+  private natural?: Order<Row>;
+
+  // Sortable columns and associated ordering (if any)
+  private columns?: ByColumns<Row>;
+
+  // Comparison Ring
+  private ring: SortingInfo[] = [];
+
+  // Consolidated order (computed on demand)
+  private order?: SORT<Key, Row>;
+
+  // Lazily compute order
+  protected sorter(): SORT<Key, Row> {
+    let current = this.order;
+    if (current) return current;
+    current = this.order = orderByRing(this.natural, this.columns, this.ring);
+    return current;
+  }
+
+  // Lazily compute table
+  protected rebuild(): TABLE<Key, Row> {
+    const current = this.table;
+    if (current !== undefined) return current;
+    let table: TABLE<Key, Row> = [];
+    try {
+      this.index.forEach((packed) => {
+        packed.index = undefined;
+        const phi = this.filter;
+        if (!phi || phi(packed.row, packed.key))
+          table.push(packed);
+      });
+      table.sort(this.sorter());
+    } catch (err) {
+      console.warn('[Dome] error when rebuilding table:', err);
+    }
+    table.forEach((pack, index) => pack.index = index);
+    this.table = table;
+    return table;
+  }
+
+  // --------------------------------------------------------------------------
+  // --- Proxy
+  // --------------------------------------------------------------------------
+
+  getRowCount() { return this.rebuild().length; }
+
+  getRowAt(k: number) { return this.rebuild()[k]?.row; }
+
+  getKeyAt(k: number) {
+    const current = this.table;
+    return current ? current[k]?.key : undefined;
+  }
+
+  getKeyFor(k: number, _: Row) { return this.getKeyAt(k); }
+
+  getIndexOf(key: Key) {
+    const pack = this.index.get(key);
+    if (!pack) return undefined;
+    const k = pack.index;
+    if (k === undefined || k < 0) return undefined;
+    const current = this.table;
+    return (current && k < current.length) ? k : undefined;
+  }
+
+  // --------------------------------------------------------------------------
+  // --- Ordering
+  // --------------------------------------------------------------------------
+
+  /** Sets comparison functions for the specified columns. Previous
+      comparison for un-specified columns are kept unchanged, if any.
+      This will be used to refine
+      [[setNaturalOrder]] in response to user column selection with
+      [[setSortBy]] provided you enable by-column sorting from the table view.
+      Finally triggers a reload. */
+  setColumnOrder(columns?: ByColumns<Row>) {
+    this.columns = { ...this.columns, ...columns };
+    this.reload();
+  }
+
+  /** Sets natural ordering of the rows.
+      It defines in which order the entries are rendered in the table.  This
+      primary ordering can be refined in response to user column selection with
+      [[setSortBy]] provided you enable by-column sorting from the table view.
+      Finally triggers a reload. */
+  setNaturalOrder(order?: Order<Row>) {
+    this.natural = order;
+    this.reload();
+  }
+
+  /**
+     Sets both natural ordering and column ordering with the provided
+     orders by fields. This is a combination of [[setColumnOrder]] and
+     [[setNaturalOrder]] with [[Compare.byFields]].
+   */
+  setOrderingByFields(byfields: ByFields<Row>) {
+    this.natural = Compare.byFields(byfields);
+    const columns = this.columns ?? {};
+    for (let k of Object.keys(byfields)) {
+      const dataKey = k as (string & keyof Row);
+      const fn = byfields[dataKey];
+      if (fn) columns[dataKey] = (x: Row, y: Row) => {
+        const dx = x[dataKey];
+        const dy = y[dataKey];
+        if (dx === dy) return 0;
+        if (dx === undefined) return 1;
+        if (dy === undefined) return -1;
+        return fn(dx, dy);
+      };
+    }
+    this.columns = columns;
+    this.reload();
+  }
+
+  /**
+     Remove the sorting function for the provided column.
+   */
+  deleteColumnOrder(dataKey: string) {
+    const columns = this.columns;
+    if (columns) delete columns[dataKey];
+    this.ring = this.ring.filter(ord => ord.sortBy !== dataKey);
+    this.reload();
+  }
+
+  /** Reorder rows with the provided column and direction.
+      Previous ordering is kept and refined by the new one.
+      Use `undefined` or `null` to reset the natural ordering. */
+  setSorting(ord?: undefined | null | SortingInfo) {
+    if (ord) {
+      const ring = this.ring;
+      const cur = ring[0];
+      const fd = ord.sortBy;
+      if (
+        !cur ||
+        cur.sortBy !== fd ||
+        cur.sortDirection !== ord.sortDirection
+      ) {
+        const newRing = ring.filter((o) => o.sortBy !== fd);
+        newRing.unshift(ord);
+        this.ring = newRing;
+        this.reload();
+      }
+    } else {
+      if (this.ring.length > 0) {
+        this.ring = [];
+        this.reload();
+      }
+    }
+  }
+
+  canSortBy(column: string) {
+    const columns = this.columns as any;
+    return columns[column] !== undefined;
+  }
+
+  getSorting(): SortingInfo | undefined {
+    return this.ring[0];
+  }
+
+  // --------------------------------------------------------------------------
+  // --- Filtering
+  // --------------------------------------------------------------------------
+
+  setFilter(fn?: Filter<Key, Row>) {
+    const phi = this.filter;
+    if (phi !== fn) {
+      this.filter = fn;
+      this.reload();
+    }
+  }
+
+  // --------------------------------------------------------------------------
+  // --- Full Updates
+  // --------------------------------------------------------------------------
+
+  /** Trigger a complete reload of the table. */
+  reload() {
+    if (this.table || this.order) {
+      this.table = undefined;
+      this.order = undefined;
+      super.reload();
+    }
+  }
+
+  /** Remove all data and reload. */
+  clear() {
+    this.index.clear();
+    this.reload();
+  }
+
+  // --------------------------------------------------------------------------
+  // --- Checks for Reload vs. Update
+  // --------------------------------------------------------------------------
+
+  private needReloadForUpdate(pack: PACK<Key, Row>): boolean {
+    // Case where reload is already triggered
+    const current = this.table;
+    if (!current) return false;
+    // Case where filtering of key has changed
+    const k = pack.index ?? -1;
+    const n = current ? current.length : 0;
+    const phi = this.filter;
+    const old_ok = 0 <= k && k < n;
+    const now_ok = phi ? phi(pack.row, pack.key) : true;
+    if (old_ok !== now_ok) return true;
+    // Case where element was not displayed and will still not be
+    if (!old_ok) return false;
+    // Detecting if ordering is preserved
+    const order = this.sorter();
+    const prev = k - 1;
+    if (0 <= prev && order(pack, current[prev]) < 0) return true;
+    const next = k + 1;
+    if (next < n && order(current[next], pack) < 0) return true;
+    super.updateIndex(k);
+    return false;
+  }
+
+  private needReloadForInsert(pack: PACK<Key, Row>): boolean {
+    // Case where reload is already triggered
+    const current = this.table;
+    if (!current) return false;
+    // Case where inserted element is filtered out
+    const phi = this.filter;
+    return phi ? phi(pack.row, pack.key) : true;
+  }
+
+  private needReloadForRemoval(pack: PACK<Key, Row>): boolean {
+    // Case where reload is already triggered
+    const current = this.table;
+    if (!current) return false;
+    // Case where inserted element is filtered out
+    const k = pack.index ?? -1;
+    return 0 <= k && k < current.length;
+  }
+
+  // --------------------------------------------------------------------------
+  // --- Update item and optimized reload
+  // --------------------------------------------------------------------------
+
+  /**
+     Update a data entry and signal the views only if needed.
+     Use `undefined` to keep value unchanged, `null` for removal,
+     or the new row data for update. This triggers a full
+     reload if ordering or filtering if modified by the updated value,
+     a update index if the row data is only modified and visible.
+     Otherwise, no rendering is triggered since the modification
+     is not visible.
+     @param key - the entry identifier
+     @param row - new value of `null` for removal
+   */
+  update(key: Key, row?: undefined | null | Row) {
+    if (row === undefined) return;
+    const pack = this.index.get(key);
+    let doReload = false;
+    if (pack) {
+      if (row === null) {
+        // Removal
+        this.index.delete(key);
+        doReload = this.needReloadForRemoval(pack);
+      } else {
+        // Updated
+        pack.row = row;
+        doReload = this.needReloadForUpdate(pack);
+      }
+    } else {
+      if (row === null) {
+        // Nop
+        return;
+      } else {
+        const newPack = { key, row, index: undefined };
+        this.index.set(key, newPack);
+        doReload = this.needReloadForInsert(newPack);
+      }
+    }
+    if (doReload) this.reload();
+  }
+
+  // --------------------------------------------------------------------------
+  // ---  Batched Updates
+  // --------------------------------------------------------------------------
+
+  /**
+     Silently removes the entry.
+     Modification will be only visible after a final [[reload]].
+     Useful for a large number of batched updates.
+  */
+  removeAllData() {
+    this.index.clear();
+  }
+
+  /**
+     Silently removes the entry.
+     Modification will be only visible after a final [[reload]].
+     Useful for a large number of batched updates.
+     @param key - the removed entry.
+   */
+  removeData(key: Key) {
+    this.index.delete(key);
+  }
+
+  /**
+     Silently updates the entry.
+     Modification will be only visible after a final [[reload]].
+     Useful for a large number of batched updates.
+     @param key - the entry to update.
+     @param row - the new row data or `null` for removal.
+   */
+  setData(key: Key, row: null | Row) {
+    if (row !== null) {
+      this.index.set(key, { key, row, index: undefined });
+    } else {
+      this.index.delete(key);
+    }
+  }
+
+  /** Returns the data associated with a key (if any). */
+  getData(key: Key): Row | undefined {
+    return this.index.get(key)?.row;
+  }
+
+}
+
+// --------------------------------------------------------------------------
+// --- Compact Array Model
+// --------------------------------------------------------------------------
+
+/**
+   @template Row - object data that also contains « key »
+*/
+export class ArrayModel<Row> extends MapModel<string, Row> {
+
+  private key: keyof Row
+
+  /** @param key - the key property of `Row` holding an entry identifier. */
+  constructor(key: keyof Row) {
+    super();
+    this.key = key;
+  }
+
+  /** Optimized, see [[getKey]]. */
+  getKeyFor(_: number, data: Row) { return this.getKey(data); }
+
+  /** Returns the key of data. */
+  getKey(data: Row): string { return (data as any)[this.key]; }
+
+  /** Adds a collection of data. Finally triggers a reload. */
+  add(data: Collection<Row>) {
+    forEach(data, (row: Row) => this.setData(this.getKey(row), row));
+    this.reload();
+  }
+
+  /** Replaces all previous entries with new ones. Finally triggers a reload. */
+  replace(data: Collection<Row>) {
+    this.removeAllData();
+    this.add(data);
+  }
+
+  /** Removes a colllection of data, identified by keys or (key of) rows.
+      Finally triggers a reload. */
+  remove(data: Collection<string | Row>) {
+    forEach(data, e => {
+      const k = typeof e === 'string' ? e : this.getKey(e);
+      this.removeData(k);
+    });
+    this.reload();
+  }
+
+}
+
+// --------------------------------------------------------------------------
diff --git a/ivette/src/dome/src/renderer/table/models.js b/ivette/src/dome/src/renderer/table/models.js
deleted file mode 100644
index 8a1dc224ce856ce67825653891a2fe2ba8ae1acd..0000000000000000000000000000000000000000
--- a/ivette/src/dome/src/renderer/table/models.js
+++ /dev/null
@@ -1,248 +0,0 @@
-// --------------------------------------------------------------------------
-// --- Models
-// --------------------------------------------------------------------------
-
-/**
-   @packageDocumentation
-   @module dome/table/models
-*/
-
-import _ from 'lodash' ;
-import { SortDirection } from 'react-virtualized' ;
-
-// --------------------------------------------------------------------------
-// --- Sorting
-// --------------------------------------------------------------------------
-
-/** Ascending Order (SortDirection) */
-export const ASC = SortDirection.ASC ;
-
-/** Descending Order (SortDirection) */
-export const DESC = SortDirection.DESC ;
-
-// --------------------------------------------------------------------------
-// --- Collection Model
-// --------------------------------------------------------------------------
-
-/**
-   @class
-   @summary Data Listener.
-   @description
-
-   A Model is responsible for keeping the tables and lists views in sync
-   with their associated data sets. The model listens for updates, retrieves
-   items from their index, and re-render the views when necessary.
-
-   Several tables may connect to the same table model, but they will share the
-   same number and ordering of items. However, each connected table will only
-   render its own range of items and will re-renderer only when impacted by
-   individual updates.
-
-
-   A Model instance may not hold the data directly. A table or list component
-   uses the model only as a _proxy_ reponsible for fetching the items with respect to
-   some current filtering and ordering selection. The model also serves as
-   a proxy for triggering table re-rendering when table data is updated.
-
-
-   To design your data model, you shall extends the base `Model` class and
-   override the public methods to fit your needs.
-
-   - `getItemCount() -> number` (the number of items)
-   - `getItemAt(number) -> item` (the item at the given index)
-   - `getIndexOf(item) -> number` (the index of an item in the current order)
-   - `getValue(item,column) -> any` (the value associated to some item in a column)
-
-
-   To implement sorting, you shall also override the following methods:
-
-   - `getOrdering() -> { sortBy, sortDirection }`
-   - `setOrdering({ sortBy, sortDirection }) -> ()`
-
-
-   Whenever data is added, removed, updated or re-ordered, the `Model` shall be
-   informed by calling one of the following methods:
-
-   - `updateItem(item);` when an individual item shall be re-rendered (if ever visible)
-   - `updateIndex(index[,index]);` when an item or a range of items shall be re-rendered
-   - `reload();` for all other modifications of the collection, including filtering and re-ordering
-
-
-   Items count and items indices shall be consistent with the current filter(s)
-   and order(s) selected by the user. Tables are equipped with
-   callbacks on table headers that can be used to trigger re-ordering of your data, but
-   you can implement your own controls or use menus to do that.
-
-   Items can be represented by any javascript values (string, integers, objects...).
-   Default table cell renderers expect items to be object with one property per column, but you
-   can override those default. The default `getValue` implementation simply returns the
-   item property corresponding to the column identifier.
-
-   When some data is updated, selection and scrolling of the views will be
-   preserved based on item's value. Table and List views will
-   keep each rendered item in sync with their index thanks to methods `getItemAt`
-   and `getIndexOf` that you provide with the Model.
-
-   Hence, items implementation shall contains enough information to uniquely identify them,
-   whatever their current index.
-
-   ##### Model Helpers
-
-   The module [[dome/table/arrays]] provides you with
-   usefull helpers to implement Models with filtering and ordering features.
-
-*/
-export class Model {
-
-  constructor() {
-    this._dome_clients = {} ;
-    this._dome_clientId = 0 ;
-  }
-
-  /**
-     @summary Items count.
-     @abstract
-     @return {number} number of items
-     @description
-     Shall return the number of items to be displayed by the table.
-     Negative values are considered as zero.
-     Default implementation returns zero.
-   */
-  getItemCount() { return 0; }
-
-  /**
-     @summary Item at given index.
-     @abstract
-     @param {number} index - item's ordering index
-     @return {any} the item's value
-     @description
-     Shall return the item at a given index in the table with respect to
-     current filtering and ordering (if appropriate).
-     <p>
-     Default implementation returns `undefined`.
-  */
-  getItemAt() { return undefined; }
-
-  /**
-     @summary Index of an item.
-     @abstract
-     @param {any} item - the item
-     @return {number} index of the item in the filtered and ordered collection
-     @description
-     Shall return the index of a given item inside the table with respect to
-     current filtering and ordering, or `undefined` if no such item exists.
-     <p>
-     Default implementation returns `undefined`.
-  */
-  getIndexOf() { return undefined; }
-
-  /**
-     @summary Item value in a column.
-     @param {any} item - an item
-     @param {string} column - a column identifier
-     @return {any} the value associated to the item for the given column.
-     @description
-     Defaults to accessing the column property of the item (ie. `item[column]`).
-     This method can be overriden by custom models and also table columns.
-   */
-  getValue(item,column) { return item[column]; }
-
-  /**
-     @summary Re-render an item.
-     @param {any} item - the updated item
-     @description
-     Signal that a given item has been updated and need to be re-rendered if visible.
-  */
-  updateItem(item) {
-    const k = this.getIndexOf(item);
-    if ( 0 <= k ) this.updateIndex(k);
-  }
-
-  /**
-     @summary Re-render a range of items.
-     @param {number} first - the first updated item index
-     @param {number} [last] - the last updated item index (defaults to `first`)
-     @description
-     Signal that a range of items have been updated and need to be re-rendered if visible.
-  */
-  updateIndex(a,b=a) {
-    _.forOwn(this._dome_clients,({ lower,upper,trigger }) => {
-      if ( a <= upper && lower <= b ) trigger();
-    });
-  }
-
-  /** Re-render all items */
-  reload() { _.forOwn(this._dome_clients,({trigger})=> trigger()); }
-
-  /**
-     @summary Current ordering.
-     @return {object} current sorting order
-     @description
-     Shall return the current ordering `{ sortBy, sortDirection }`
-     for user feedback in table headers, and `undefined` for natural ordering
-     or no ordering at all.
-     <p>
-     Default implementation returns `undefined`.
-  */
-  getOrdering() { return undefined; }
-
-  /**
-     @summary Change ordering of the model.
-     @param {object} [sort] - sorting order
-     @description
-     Callback to user clicks on table headers. This method receives
-     the new `{ sortBy, sortDirection }` ordering requested by the user action,
-     of `undefined` to reset initial, natural ordering of items.
-     You can also invoke this method on your own, away from any table view.
-     <p>
-     The method shall eventually reorder the items internally, and finally
-     signal completion with a call to `Model.reload()` in order to sync the views.
-     If re-ordering can take a while, this shall be performed asynchronously.
-     <p>
-     Default implementation does nothing.
-  */
-  setOrdering() { }
-
-  /**
-     @summary Connect a trigger to the model.
-     @protected
-     @param {Function} trigger - callback to force table update
-     @return {ClientID} client identifier
-     @description
-     Returns a _client_ identifier for removing and watching.
-     The trigger function is called for each update watched by the _client_.
-     Initially, the watching range is empty.
-  */
-  _bind(trigger) {
-    const client = "#" + this._dome_clientId++ ;
-    this._dome_clients[client] = { lower:0, upper:0, trigger };
-    return client;
-  }
-
-  /**
-     @summary Disconnect the _client_ from the model.
-     @protected
-     @param {ClientID} client - the identifier of the client to disconnect
-  */
-  _remove(client) {
-    delete this._dome_clients[client];
-  }
-
-  /**
-     @summary Set the current range of items watched by the _client_.
-     @protected
-     @param {ClientID} client - the identifier of the client to disconnect
-     @param {number} first - first index of the range
-     @param {number} last - last index of the range
-     @description
-     Data updates tha fall outside this range will _not_ trigger
-     re-rendering of the client.
-  */
-  _watch(client,a,b) {
-    const listener = this._dome_clients[client];
-    if (listener) { listener.lower = a ; listener.upper = b; }
-  }
-
-}
-
-// --------------------------------------------------------------------------
diff --git a/ivette/src/dome/src/renderer/table/models.ts b/ivette/src/dome/src/renderer/table/models.ts
new file mode 100644
index 0000000000000000000000000000000000000000..78bad99218c8ede24ff5dde5fc13e1bd1b292245
--- /dev/null
+++ b/ivette/src/dome/src/renderer/table/models.ts
@@ -0,0 +1,221 @@
+// --------------------------------------------------------------------------
+// --- Models
+// --------------------------------------------------------------------------
+
+/**
+   @packageDocumentation
+   @module dome/table/models
+*/
+
+import { SortDirectionType } from 'react-virtualized';
+
+// --------------------------------------------------------------------------
+// --- Listeners
+// --------------------------------------------------------------------------
+
+/** Update callback. */
+export type Trigger = () => void;
+
+/** Client Views. */
+export interface Client {
+  /** Set the update callback of the client. */
+  onUpdate(trigger?: Trigger): void;
+  /** Set the reload callback of the client. */
+  onReload(trigger?: Trigger): void;
+  /** Set the watching range of the client. */
+  watch(lower: number, upper: number): void;
+  /** Unlink the client. */
+  unlink(): void;
+}
+
+/** @internal */
+interface Watcher {
+  lower: number;
+  upper: number;
+  update: undefined | Trigger;
+  reload: undefined | Trigger;
+}
+
+// --------------------------------------------------------------------------
+// --- Sorting
+// --------------------------------------------------------------------------
+
+/** Sorting Info. */
+export interface SortingInfo {
+  /** The column identifier that triggers some sorting. */
+  sortBy: string;
+  /** The requested sorting direction (`'ASC'` or `'DESC'`). */
+  sortDirection: SortDirectionType;
+}
+
+/** Sorting proxy.
+    Can be provided along with Models or in a separate class or object. */
+export interface Sorting {
+  /** Whether the model can be sorted from the `dataKey` column identifier. */
+  canSortBy(dataKey: string): boolean;
+  /** Callback to respond to sorting requests from columns. */
+  setSorting(order?: SortingInfo): void;
+  /** Current sorting information. */
+  getSorting(): SortingInfo | undefined;
+}
+
+// --------------------------------------------------------------------------
+// --- Filtering
+// --------------------------------------------------------------------------
+
+export interface Filter<Key, Row> {
+  (row: Row, key: Key): boolean;
+}
+
+export interface Filtering<Key, Row> {
+  setFilter(fn?: Filter<Key, Row>): void;
+}
+
+// --------------------------------------------------------------------------
+// --- Collection
+// --------------------------------------------------------------------------
+
+/** Convenient type for a collection of items. */
+export type Collection<A> = undefined | null | A | Collection<A>[];
+
+/** Iterator over collection. */
+export function forEach<A>(data: Collection<A>, fn: (elt: A) => void) {
+  if (Array.isArray(data)) data.forEach((e) => forEach(e, fn));
+  else if (data !== undefined && data !== null) fn(data);
+}
+
+// --------------------------------------------------------------------------
+// --- Abstract Model
+// --------------------------------------------------------------------------
+
+/**
+   A Model is responsible for keeping the tables and lists views in sync
+   with their associated data sets. The model listens for updates, retrieves
+   items from their index, and re-render the views when necessary.
+
+   Several tables may connect to the same table model, but they will share the
+   same number and ordering of items. However, each connected table will only
+   render its own range of items and will re-render only when impacted by
+   individual updates.
+
+   The model might not hold the entire collection of data at the same time, but
+   serves as a proxy for fetching data on demand. A model makes a distinction between:
+   - `Key`: a key identifies a given entry in the table at any time;
+   - `Row`: the row data associated to some `Key` at a given time;
+
+   When row data change over time, the table views associated to the model
+   use `Key` information to keep scrolling and selection states in sync.
+
+   The model is responsible for:
+   - providing row data to the views;
+   - informing views of data updates;
+   - compute row ordering with respect to ordering and/or filtering;
+   - lookup key index with respect to ordering and/or filtering;
+
+   When your data change over time, you shall invoke the following methods
+   of the model to keep views in sync:
+   - [[update]] or [[updateIndex]] when single or contiguous row data changes over time;
+   - [[reload]] when the number of rows, their ordering, or (many) row data has been changed.
+
+   It is always safe to use `reload` instead of `update` although it might be less performant.
+
+   @template Key - identification of some entry
+   @template Row - dynamic row data associated to some key
+*/
+export abstract class Model<Key, Row> {
+
+  private clients = new Map<number, Watcher>();
+  private clientsId = 0;
+
+  /**
+     Shall return the number of rows to be currently displayed in the table.
+     Negative values are considered as zero.
+  */
+  abstract getRowCount(): number;
+
+  /**
+     Shall return the current row data at a given index in the table, with respect to
+     current filtering and ordering (if any).
+     Might return `undefined` if the index is invalid or not (yet) available.
+  */
+  abstract getRowAt(index: number): undefined | Row;
+
+  /**
+     Shall return the key at the given index. The specified index and data
+     are those of the last corresponding call to [[getRowAt]].
+     Might return `undefined` if the index is invalid.
+  */
+  abstract getKeyAt(index: number): undefined | Key;
+
+  /**
+     Shall return the key of the given entry. The specified index and data
+     are those of the last corresponding call to [[getRowAt]].
+     Might return `undefined` if the index is invalid.
+  */
+  abstract getKeyFor(index: number, data: Row): undefined | Key;
+
+  /**
+     Shall return the index of a given entry in the table, identified by its key, with
+     respect to current filtering and ordering (if any).
+     Shall return `undefined` if the specified key no longer belong to the table or
+     when it is currently filtered out.
+     Out-of-range indices would be treated as `undefined`.
+  */
+  abstract getIndexOf(key: Key): undefined | number;
+
+  /**
+     Signal an item update.
+     Default implementation uses [[getIndexOf]] to retrieve the index and then
+     delegates to [[updateIndex]].
+     All views that might be rendering the specified item will be updated.
+  */
+  update(key: Key) {
+    const k = this.getIndexOf(key);
+    if (k !== undefined && 0 <= k) this.updateIndex(k);
+  }
+
+  /**
+     Signal a range of updates.
+     @param first - the first updated item index
+     @param last - the last updated item index (defaults to `first`)
+  */
+  updateIndex(first: number, last = first) {
+    if (first <= last) {
+      this.clients.forEach(({ lower, upper, update }) => {
+        if (update && first <= upper && lower <= last) update();
+      });
+    }
+  }
+
+  /** Re-render all views. */
+  reload() { this.clients.forEach(({ reload }) => reload && reload()); }
+
+  /**
+     Connect a client view to the model.
+     The initial watching range is empty with no trigger.
+     You normally never call this method directly.
+     It is automatically called by table views.
+  */
+  link(): Client {
+    const id = this.clientsId++;
+    const m = this.clients;
+    const w: Watcher & Client = {
+      lower: 0,
+      upper: 0,
+      update: undefined,
+      reload: undefined,
+      onUpdate(s?: Trigger) { w.update = s; },
+      onReload(s?: Trigger) { w.reload = s; },
+      unlink() { m.delete(id); },
+      watch(lower: number, upper: number) {
+        w.lower = lower;
+        w.upper = upper;
+      }
+    };
+    m.set(id, w);
+    return w;
+  }
+
+}
+
+// --------------------------------------------------------------------------
diff --git a/ivette/src/dome/src/renderer/table/style.css b/ivette/src/dome/src/renderer/table/style.css
index bf732426f386057343a6d9a1cf89ca777e3b2f10..aefbe542e53a14750f730b645303516c8a51ecb5 100644
--- a/ivette/src/dome/src/renderer/table/style.css
+++ b/ivette/src/dome/src/renderer/table/style.css
@@ -66,6 +66,10 @@
     margin-right: 2px ;
 }
 
+.dome-xTable-renderer {
+    overflow: hidden;
+}
+
 .dome-xTable-resizer {
     position: absolute ;
     cursor: col-resize ;
@@ -96,6 +100,14 @@
     vertical-align: baseline ;
 }
 
+.dome-window-active .dome-xTable-selected {
+    background: #8ce0fb ;
+}
+
+.dome-window-inactive .dome-xTable-selected {
+    background: #ccc ;
+}
+
 .dome-xTable-odd {
     background: #fdfdfd ;
 }
@@ -119,3 +131,17 @@
 }
 
 /* -------------------------------------------------------------------------- */
+/* --- Dragging Columns                                                   --- */
+/* -------------------------------------------------------------------------- */
+
+.dome-xTable .ReactVirtualized__Table__headerColumn,
+.dome-xTable .ReactVirtualized__Table__rowColumn
+{
+    transition: flex-basis linear 50ms ;
+}
+
+.dome-xTable-resizer {
+    transition: left linear 50ms ;
+}
+
+/* -------------------------------------------------------------------------- */
diff --git a/ivette/src/dome/src/renderer/table/views.js b/ivette/src/dome/src/renderer/table/views.js
deleted file mode 100644
index dc7923748ce90485095863132c57818cb7823f00..0000000000000000000000000000000000000000
--- a/ivette/src/dome/src/renderer/table/views.js
+++ /dev/null
@@ -1,667 +0,0 @@
-// --------------------------------------------------------------------------
-// --- Tables
-// --------------------------------------------------------------------------
-
-/**
-   @packageDocumentation
-   @module dome/table/views
-*/
-
-import _ from 'lodash' ;
-import React from 'react' ;
-import * as Dome from 'dome' ;
-import { DraggableCore } from 'react-draggable';
-import { SVG } from 'dome/controls/icons' ;
-import {
-  AutoSizer,
-  SortDirection,
-  Table as VTable,
-  Column as VColumn
-} from 'react-virtualized' ;
-
-import './style.css' ;
-
-// --------------------------------------------------------------------------
-// --- Header Renderer
-// --------------------------------------------------------------------------
-
-const headerRowRenderer =
-      (contextMenu) =>
-      // Borrowed from react-virtualized Table.defaultHeaderRowRenderer
-      ({
-        className,
-        columns,
-        style
-      }) => (
-        <div role="row"
-             className={className}
-             style={style}
-             onContextMenu={contextMenu} >
-          {columns}
-        </div>
-      );
-
-const headerIcon = (icon) => (
-  icon && <div className='dome-xTable-header-icon'><SVG id={icon}/></div>
-);
-
-const headerLabel = (label) => (
-  label &&
-    (<label className='dome-xTable-header-label dome-text-label'>
-     {label}
-     </label>)
-);
-
-const makeSorter = (id) => (
-  <div className='dome-xTable-header-sorter'>
-    <SVG id={id} size={8}/>
-  </div>
-);
-
-const headerSorter = {} ;
-headerSorter[ SortDirection.ASC ] = makeSorter('ANGLE.UP');
-headerSorter[ SortDirection.DESC ] = makeSorter('ANGLE.DOWN');
-
-const headerRenderer =
-      ({
-        columnData: { label, icon, title, headerRef },
-        dataKey,
-        sortBy,
-        sortDirection
-      }) => {
-        const tooltip = title || label ;
-        const onRef = (elt) => headerRef(dataKey,elt) ;
-        const sorter = dataKey === sortBy ? headerSorter[sortDirection] : undefined ;
-        return (
-          <div className='dome-xTable-header' title={tooltip} ref={onRef} >
-            { headerIcon(icon) }
-            { headerLabel(label) }
-            { sorter }
-          </div>
-        );
-      };
-
-// --------------------------------------------------------------------------
-// --- Cell Renderer
-// --------------------------------------------------------------------------
-
-const cellDataGetter =
-      (getValue) =>
-      ({ rowData:{ model , item }, dataKey:id }) =>
-      ( item == undefined ? undefined :
-        getValue ? getValue(item) :
-        model.getValue(item,id)
-      );
-
-const cellRenderer =
-      (renderValue) =>
-      ({ cellData: data }) =>
-      (
-        data === undefined ? undefined :
-        renderValue ? renderValue(data) :
-        (<div className='dome-xTable-cell dome-text-data'>{data}</div>)
-      );
-
-// --------------------------------------------------------------------------
-// --- Column Resizer
-// --------------------------------------------------------------------------
-
-const DRAGGING = 'dome-xTable-resizer dome-color-dragging' ;
-const DRAGZONE = 'dome-xTable-resizer dome-color-dragzone' ;
-
-const Resizer = (props) => (
-  <DraggableCore
-    onStart={props.onStart}
-    onStop={props.onStop}
-    onDrag={(_elt,data)=> props.onDrag(props.left,props.right,data.x - props.offset)}
-    >
-    <div
-      className={ props.id === props.dragging ? DRAGGING : DRAGZONE }
-      style={{ left: props.offset-2 }}
-      />
-  </DraggableCore>
-);
-
-const computeWidth = (elt) => {
-  const parent = elt && elt.parentElement ;
-  return parent && parent.getBoundingClientRect().width ;
-};
-
-// --------------------------------------------------------------------------
-// --- Table Columns
-// --------------------------------------------------------------------------
-
-/**
-   @summary Table Column.
-   @property {string} id - Column unique identifier (required)
-   @property {string} [icon] - Header icon
-   @property {string} [label] - Header label
-   @property {string} [title] - Header tooltip
-   @property {string} [align] - Column alignment (`'left'`, `'center'`, `'right'`)
-   @property {number} [width] - Column base width (in pixels, default `60`)
-   @property {boolean} [fill] - Extensible column (not by default)
-   @property {boolean} [fixed] - Non-resizable column (not by default)
-   @property {boolean} [disableSort] - Do not trigger sorting callback for this column
-   @property {boolean|string} [visible] - Default column visibility
-   @property {function} [getValue] - Obtain an item's value for this column
-   @property {function} [renderValue] - Render item's value in each table cell
-   @description
-
-   Each column displays a specific value derived from the item displayed in a
-   row. Column properties enforce a separation between how to extract the value
-   from an item and how to render it in the cell.
-
-   - `getValue(item) : any` shall returns the value to render for the _item_
-   - `renderValue(any) : Element` shall returns the (React) element to display the item
-
-
-   By default, values are obtained from the underlying model by invoking
-   [[Model.getValue]] with the column identifier.
-
-   This separation of concerns allows for defining
-   Column types, where for instance the renderer is already defined and you only need to
-   know how to extract the expected value of items.
-   See [[DefineColumn]]
-   for more informations and examples.
-
-   A table should have at least one extensible column to occupy the available width.
-   If no column in the table is explicitely declared to be extensible, the last
-   one would be implicitely set to fill.
-
-   Default visiblity can be set to a boolean value ; alternatively, you may specify
-   `visible='never'` to make the column invisible to the user, or `visible='always'`
-   to force the column to be visible.
-
-*/
-export const Column = (props) => null;
-// Fake component only used to store props.
-// Virtualized column is rendered with function vColumn (see below)
-
-const vColumn = ({
-  headerRef,
-  columnResize,hasFill,lastElt,
-  contextMenu
-}) => (elt) => {
-  const defaults = elt.type._DOME_COLUMN_DEFAULTS || {} ;
-  const forcers = !hasFill && elt == lastElt ? { fill:true } : {} ;
-  const { id,label,title,icon,align,width,fill,disableSort,getValue,renderValue }
-        = Object.assign( {}, defaults , elt.props , forcers ) ;
-  return (
-    <VColumn
-      key={id}
-      displayName='React-Virtualized-Column'
-      width={columnResize[id] || width || 60}
-      flexGrow={ fill ? 1 : 0 }
-      dataKey={id}
-      columnData={{label,title,icon,headerRef}}
-      headerRenderer={headerRenderer}
-      cellRenderer={cellRenderer(renderValue)}
-      cellDataGetter={cellDataGetter(getValue)}
-      headerStyle={{ textAlign: align }}
-      disableSort={disableSort}
-      style={{ textAlign: align }}
-      />
-  );
-};
-
-const defaultVisible = (visible) => {
-  switch(visible) {
-  case 'always':
-  case undefined:
-    return true;
-  case 'never':
-  case null:
-    return false;
-  default:
-    return visible;
-  }
-};
-
-// --------------------------------------------------------------------------
-// --- Specific Columns
-// --------------------------------------------------------------------------
-
-/**
-   @summary Define specific Column instances.
-   @param {Object} properties - default Column properties
-   @return {Column} a new Column class of Component
-   @description
-
-   Allow to define specialized instances of [[Column]].
-
-   @example // Example of column type
-   import { DefineColumn } from 'dome/table/views' ;
-   export const ColumnCheck = DefineColumn({
-     align: 'center',
-     renderValue: (ok) => <Icon id={ok ? 'CHECK' : 'CROSS'}/>
-   });
-
-   @example // Usage in a Table
-   <Table ...>
-      <Column id='name' label='Name' fill />
-      <ColumnCheck id='check' label='Checked' />
-   </Table>
-
- */
-export const DefineColumn = (DEFAULTS) => {
-  function Component() { return null; };
-  Component._DOME_COLUMN_DEFAULTS = DEFAULTS ;
-  return Component ;
-};
-
-// --------------------------------------------------------------------------
-// --- Table Rows
-// --------------------------------------------------------------------------
-
-const rowClassName =
-      (multipleSelection,selected) =>
-      ({index}) =>
-      (multipleSelection
-       ? 0 <= _.sortedIndexOf( selected , index )
-       : (index === selected))
-      ? 'dome-color-selected' :
-      index & 1 ? 'dome-xTable-even' : 'dome-xTable-odd' ;
-
-// --------------------------------------------------------------------------
-// --- Table View
-// --------------------------------------------------------------------------
-
-// Must be kept in sync with table.css
-const CSS_HEADER_HEIGHT = 22 ;
-const CSS_ROW_HEIGHT = 20 ;
-const DEFAULT_STATE = { width:{}, resize:{}, visible:{} };
-
-/**
-   @class
-   @summary Table View.
-   @property {Model} model - table data proxy (required)
-   @property {Column[]} children - one or more table columns (required)
-   @property {string} [settings] - window settings for column size & visibility (optional)
-   @property {any} [selection] - current selection (depends on `multipleSelection`)
-   @property {function} [onSelection] - callback to selection changes (depends on `multipleSelection`)
-   @property {boolean} [multipleSelection] - select single or multiple selection
-   @property {any} [scrollToItem] - ensures the item is visible
-   @property {function} [renderEmpty] - callback to render an empty table
-   @description
-
-   This component is base on [React-Virtualized
-   Tables](https://bvaughn.github.io/react-virtualized/#/components/Table),
-   offering a lazy, super-optimized rendering process that scales on huge
-   datasets.
-
-   A table shall be connected to an instance of
-   [[Model]] class to retrieve the data and
-   get informed of data updates.
-
-   The table columns shall be instances of
-   [[Column]] class.
-
-   Clicking on table headers trigger re-ordering callback on the model with the
-   expected column and direction, unless disabled _via_ the column
-   specification. However, actual sorting (and corresponding feedback on table
-   headers) would only take place if the model supports re-ordering and
-   eventually triggers a reload.
-
-   Right-clicking the table headers displays a popup-menu with actions to reset natural ordering,
-   reset column widths and select column visibility.
-
-   Tables do not control item selection state. Instead, you shall supply the selection
-   state and callback _via_ properties, like any other controlled React components.
-
-   Item selection can be based either on single-row or multiple-row. In case of
-   single-row selection (`multipleSelection:false`, the default), selection state
-   must be a single item or `undefined`, and the `onSelection` callback is called
-   with the same type of values.
-
-   In case of multiple-row selection (`multipleSelection:true`), the selection state
-   shall be an _array_ of items, and `onSelection` callback also. Single items are _not_
-   accepted, but `undefined` selection can be used in place of an empty array.
-
-   Clicking on a row triggers the `onSelection` callback with the updated selection.
-   In single-selection mode, the clicked item is sent to the callback. In
-   multiple-selection mode, key modifiers are taken into account for determining the new
-   slection. By default, the new selection only contains the clicked item. If the `Shift`
-   modifier has been pressed, the current selection is extended with a range of items
-   from the last selected one, to the newly selected one. If the `CtrlOrCmd` modifier
-   has been pressed, the selection is extended with the newly clicked item.
-   Clicking an already selected item with the `CtrlOrCmd` modifier removes it from
-   the current selection.
-
- */
-export class Table extends React.Component {
-
-  constructor(props)
-  {
-    super(props);
-
-    // Table Reload
-    this.tableRef = undefined ;
-    this.setTableRef = (ref) => this.tableRef = ref ;
-    this.reloadTable = () => {
-      this.reloaded = true ;
-      setImmediate(() => {
-        const ref = this.tableRef ;
-        if (ref) {
-          this.forceUpdate();
-          ref.forceUpdateGrid();
-        }
-      });
-    };
-
-    // Model Watching
-    this.watchModel = ({startIndex,stopIndex}) => {
-      this.props.model._watch( this.client , startIndex , stopIndex );
-    };
-
-    // Default Context Menu
-    this.resetOrdering = () => this.props.model.setOrdering() ;
-
-    // Column States
-    this.state = Object.assign(
-      DEFAULT_STATE,
-      Dome.getWindowSetting( this.props.settings )
-    );
-    this.restoreDefaults = () => this.setState( DEFAULT_STATE );
-
-    // Header Reset Resizing
-    this.resetResizing = () => this.setState({ width:{}, resize:{} });
-
-    // Header Column References
-    this.headerRef = (id,elt) => {
-      const old = this.state.width[id] ;
-      const current = computeWidth(elt);
-      if (elt && old !== current) {
-        const columns = Object.assign( {}, this.state.width );
-        columns[id] = current ;
-        this.setState({ width: columns });
-      }
-    };
-
-    // Column Resizing
-    this.resizeColumns = (lcol,rcol,delta) => {
-      const columnSize = this.state.width ;
-      const wl = columnSize[lcol] + delta ;
-      const wr = columnSize[rcol] - delta ;
-      if (wl > 40 && wr > 40) {
-        const resize = Object.assign( {}, this.state.resize );
-        resize[lcol] = wl ;
-        resize[rcol] = wr ;
-        this.setState({ resize });
-      }
-    };
-
-    // Column Visibility
-    this.isVisible = (visible) => (elt) => {
-      const props = elt.props ;
-      const v = visible[props.id] ;
-      if (v !== undefined) return v;
-      const p = props.visible ;
-      switch( p ) {
-      case 'never':
-      case null:
-        return false;
-      case 'always':
-      case undefined:
-        return true;
-      default:
-        return p;
-      }
-    };
-
-    // Selection
-    this.selectRow = this.selectRow.bind(this);
-    this.contextMenu = this.contextMenu.bind(this);
-
-  }
-
-  // --- Life Cycle (binding to model)
-
-  componentDidMount()
-  {
-    Dome.on('dome.defaults',this.restoreDefaults );
-    this.client = this.props.model._bind(this.reloadTable);
-  }
-
-  componentWillUnmont()
-  {
-    Dome.off('dome.defaults',this.restoreDefaults );
-    this.props.model._remove(this.client);
-    this.tableRef = undefined ;
-  }
-
-  componentDidUpdate()
-  {
-    Dome.setWindowSetting( this.props.settings, this.state );
-  }
-
-  // --- Column Resizers
-
-  computeResizers(columns) {
-    // Insert a resizer on each side of non-fixed columns,
-    // provided there also exists some non-fixed column on both side.
-    if (columns.length < 2) return null;
-    const resizing = columns.map( ({props:{id,fixed}}) => ({id,fixed}) );
-    var k, cid ;
-    for (cid = undefined, k = 0; k < columns.length; k++) {
-      const r = resizing[k];
-      r.left = cid ;
-      if (!r.fixed) cid = r.id ;
-    }
-    for (cid = undefined, k = columns.length-1; 0 <= k ; k--) {
-      const r = resizing[k];
-      r.right = cid ;
-      if (!r.fixed) cid = r.id ;
-    }
-    var offset = 0 , resizers = [] ;
-    const columnSize = this.state.width ;
-    for (k = 0; k < columns.length - 1 ; k++) {
-      const width = columnSize[resizing[k].id] ;
-      if (!width) return null;
-      offset += width ;
-      const a = resizing[k];
-      const b = resizing[k+1];
-      if ((!a.fixed || !b.fixed) && a.right && b.left) {
-        const id = k ;
-        const onStart = () => { this.dragging = id ; this.forceUpdate(); };
-        const onStop = () => { this.dragging = undefined ; this.forceUpdate(); };
-        const resizer = (
-          <Resizer key={id}
-                   id={id}
-                   dragging={this.dragging}
-                   onStart={onStart}
-                   onStop={onStop}
-                   onDrag={this.resizeColumns}
-                   offset={offset}
-                   left={b.left}
-                   right={a.right} />
-        );
-        resizers.push(resizer);
-      }
-    }
-    return resizers ;
-  }
-
-  // --- Context Menu
-
-  contextMenu() {
-    var has_order ;
-    var has_width ;
-    var has_default ;
-    const children = this.props.children ;
-    React.Children.forEach(children, (elt) => {
-      if (elt) {
-        const { fixed, disableSort, visible } = elt.props ;
-        if (!disableSort) has_order = true ;
-        if (!fixed) has_width = true ;
-        if (visible!=='always' && visible!=='never')
-          has_default = true ;
-      }
-    });
-    const items = [
-      { label: 'Reset Ordering',
-        display:has_order, onClick:this.resetOrdering },
-      { label: 'Reset Column widths',
-        display:has_width, onClick:this.resetResizing },
-      { label: 'Restore Columns defaults',
-        display:has_default, onClick:this.restoreDefaults },
-      'separator'
-    ];
-    const visible = Object.assign( {}, this.state.visible );
-    React.Children.forEach(children, (elt) => {
-      if (elt) {
-        switch(elt.props.visible) {
-        case 'never':
-        case 'always':
-          break;
-        default:
-          const { id, label, title } = elt.props ;
-          const checked = this.isVisible(visible)(elt);
-          const onClick = () => {
-            visible[id] = !checked ;
-            this.setState({ visible });
-          };
-          items.push({ label: label || title, checked, onClick });
-        }
-      }
-    });
-    Dome.popupMenu(items);
-  }
-
-  // --- Row Selection
-
-  selectRow({event, index, rowData:{item}}) {
-    this.focus = item ;
-    if (item) {
-      const { model, multipleSelection , selection, onSelection } = this.props ;
-      if (multipleSelection) {
-        const selectedItems =
-              selection === undefined ? [] :
-              Array.isArray(selection) ? selection :
-              [selection] ;
-        if (event.metaKey || event.ctrlKey) {
-          var s, a ;
-          const isClicked = (e) => model.getIndexOf(e) === index ;
-          if (_.find( selectedItems , isClicked )) {
-            s = _.filter( selectedItems, (e) => model.getIndexOf(e) !== index );
-          } else {
-            s = selectedItems.slice();
-            s.push(item);
-            a = index ;
-          }
-          this.anchor = a ;
-          this.anchored = undefined ;
-          onSelection(s);
-        }
-        else if (event.shiftKey && this.anchor) {
-          var old = this.anchored || (this.anchored = selection) ;
-          var updated = old.slice();
-          var anchor = this.anchor ;
-          var k ;
-          if (anchor < index)
-            for (k = anchor ; k <= index ; k++) {
-              updated.push(model.getItemAt(k));
-            }
-          else
-            for (k = anchor ; index <= k ; k--) {
-              updated.push(model.getItemAt(k));
-            }
-          // No anchor modification
-          onSelection(_.uniqBy(updated, model.getIndexOf.bind(model)));
-        }
-        else {
-          this.anchor = index ;
-          this.anchored = undefined ;
-          onSelection([item]);
-        }
-      } else {
-        onSelection(item);
-      }
-    }
-  }
-
-  // --- Rendering
-
-  render() {
-
-    const {
-      model, renderEmpty,
-      multipleSelection, selection, onSelection,
-      scrollToItem
-    } = this.props ;
-
-    const itemCount = model.getItemCount();
-    const ordering = model.getOrdering();
-    var selected = undefined ;
-    if (selection)
-      if (multipleSelection && Array.isArray(selection)) {
-        selected = selection.map((elt) => {
-          var k = model.getIndexOf(elt);
-          return Number.isInteger(k) ? k : -1 ;
-        }).sort((a,b) => a-b);
-      } else
-        selected = model.getIndexOf(selection);
-
-    const rowGetter = ({index}) => ({
-      model , item: (index < itemCount ? model.getItemAt(index) : undefined)
-    }) ;
-
-    const isVisible = this.isVisible(this.state.visible);
-    const columns = React.Children.toArray(this.props.children).filter(isVisible);
-    var hasFill = false ;
-    var lastElt = undefined ;
-    React.Children.forEach(columns,(elt) => {
-      if (elt.props.fill) hasFill = true ; else lastElt = elt ;
-    });
-    const SizedTable = ({ height, width }) => {
-      const tableHeight = CSS_HEADER_HEIGHT + CSS_ROW_HEIGHT * itemCount ;
-      const smallHeight = itemCount > 0 && tableHeight < height ;
-      const rowCount = ( smallHeight ? itemCount + 1 : itemCount) ;
-      const reloaded = this.reloaded ;
-      if (reloaded) this.reloaded = false ;
-      const scrollToIndex =
-            scrollToItem ? model.getIndexOf(scrollToItem) :
-            reloaded && this.focus ? model.getIndexOf(this.focus) : undefined ;
-      const resizers = this.computeResizers(columns);
-      const renderColumn = vColumn({
-        headerRef: this.headerRef,
-        hasFill, lastElt,
-        columnResize:this.state.resize
-      });
-      return (
-        <React.Fragment>
-          <VTable
-            ref={this.setTableRef}
-            key='table'
-            displayName='React-Virtualized-Table'
-            width={width}
-            height={height}
-            rowCount={rowCount}
-            noRowsRenderer={renderEmpty}
-            rowGetter={rowGetter}
-            rowClassName={rowClassName(multipleSelection,selected)}
-            rowHeight={CSS_ROW_HEIGHT}
-            headerHeight={CSS_HEADER_HEIGHT}
-            headerRowRenderer={headerRowRenderer(this.contextMenu)}
-            onRowsRendered={this.watchModel}
-            onRowClick={onSelection && this.selectRow}
-            sortBy={ordering && ordering.sortBy}
-            sortDirection={ordering && ordering.sortDirection}
-            sort={model.setOrdering.bind(model)}
-            scrollToIndex={ scrollToIndex }
-            scrollToAlignment='auto'
-            >
-            {React.Children.map(columns,renderColumn)}
-          </VTable>
-          {resizers}
-        </React.Fragment>
-      );
-    };
-    return (
-      <div className='dome-xTable'>
-        <AutoSizer key='table'>{SizedTable}</AutoSizer>
-      </div>
-    );
-  }
-}
-
-// --------------------------------------------------------------------------
diff --git a/ivette/src/dome/src/renderer/table/views.tsx b/ivette/src/dome/src/renderer/table/views.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..cfb3ce310a03b6f7e3bc17275e99f364379d987d
--- /dev/null
+++ b/ivette/src/dome/src/renderer/table/views.tsx
@@ -0,0 +1,1023 @@
+// --------------------------------------------------------------------------
+// --- Tables
+// --------------------------------------------------------------------------
+
+/**
+   @packageDocumentation
+   @module dome/table/views
+ */
+
+import React, { ReactNode } from 'react';
+import { forEach, debounce } from 'lodash';
+import isEqual from 'react-fast-compare';
+import * as Dome from 'dome';
+import { DraggableCore } from 'react-draggable';
+import {
+  AutoSizer, Size,
+  SortDirection, SortDirectionType,
+  Index, IndexRange,
+  Table as VTable,
+  Column as VColumn,
+  TableHeaderRowProps,
+  TableHeaderProps,
+  TableCellDataGetter,
+  TableCellRenderer,
+  RowMouseEventHandlerParams,
+} from 'react-virtualized';
+
+import { SVG as SVGraw } from 'dome/controls/icons';
+import { Trigger, Client, Sorting, SortingInfo, Model } from './models';
+
+import './style.css';
+
+const SVG = SVGraw as (props: { id: string, size?: number }) => JSX.Element;
+
+// --------------------------------------------------------------------------
+// --- Rendering Interfaces
+// --------------------------------------------------------------------------
+
+/** Cell data renderer. */
+export type Renderer<Cell> = (data?: Cell) => null | JSX.Element;
+
+/**
+   Associates, for each field `{ fd: Cell }` in `Row`, a renderer
+   for type `Cell`.
+ */
+export type RenderByFields<Row> = {
+  [fd in keyof Row]?: Renderer<Row[fd]>;
+};
+
+// --------------------------------------------------------------------------
+// --- Table Columns
+// --------------------------------------------------------------------------
+
+/**
+   @template Row - table row data of some table entries
+   @template Cell - type of cell data to render in this column
+ */
+export interface ColumnProps<Row, Cell> {
+  /** Column identifier. */
+  id: string;
+  /** Header icon. */
+  icon?: string;
+  /** Header label. */
+  label?: string;
+  /** Header title. */
+  title?: string;
+  /**
+     Column position.
+     By default, column will appear according to their mounting order.
+   */
+  index?: number;
+  /** CSS vertical alignment on cells. */
+  align?: 'left' | 'center' | 'right';
+  /** Column base width in pixels (default 60px). */
+  width?: number;
+  /** Extensible column (not by default). */
+  fill?: boolean;
+  /** Fixed width column (not by default). */
+  fixed?: boolean;
+  /**
+     Data Key for this column. Defaults to `id`. It is used for:
+     - triggering ordering events to the model, if enabled.
+     - using by-fields table renderers, if provided.
+   */
+  dataKey?: string;
+  /**
+     Disable model sorting, even if enabled by the model
+     for this column `dataKey`. Not by default.
+   */
+  disableSort?: boolean;
+  /**
+     Default column visibility. With `'never'` or `'always'` the column
+     visibility is forced and can not be modified by the user. Otherwize,
+     the user can change visibility through the column header context menu.
+   */
+  visible?: boolean | 'never' | 'always';
+  /**
+     Data getter for this column.
+   */
+  getter?: (row: Row, dataKey: string) => Cell;
+  /**
+     Override table by-fields cell renderers.
+   */
+  render?: Renderer<Cell>;
+  /**
+     Override table right-click callback.
+   */
+  onContextMenu?: (row: Row, index: number, dataKey: string) => void;
+}
+
+// --------------------------------------------------------------------------
+// --- Table Properties
+// --------------------------------------------------------------------------
+
+/**
+   @template Key - unique identifiers of table entries
+   @template Row - data associated to each key in the table entries
+ */
+export interface TableProps<Key, Row> {
+  /** Data proxy. */
+  model: Model<Key, Row>;
+  /** Sorting Proxy. */
+  sorting?: Sorting;
+  /** Rendering by Fields. */
+  rendering?: RenderByFields<Row>;
+  /** Window settings to store the size and visibility of columns. */
+  settings?: string;
+  /** Selected row (identified by key). */
+  selection?: Key;
+  /** Selection callback. */
+  onSelection?: (row: Row, key: Key, index: number) => void;
+  /** Context menu callback. */
+  onContextMenu?: (row: Row, index: number) => void;
+  /** Fallback for rendering an empty table. */
+  renderEmpty?: () => null | JSX.Element;
+  /** Shall only contains `<Column<Row> … />` elements. */
+  children?: any;
+}
+
+// --------------------------------------------------------------------------
+// --- React-Virtualized Interfaces
+// --------------------------------------------------------------------------
+
+type divRef = React.RefObject<HTMLDivElement>;
+type tableRef = React.RefObject<VTable>;
+
+interface ColumnData {
+  icon?: string;
+  label?: string;
+  title?: string;
+  headerMenu: () => void;
+  headerRef: divRef;
+};
+
+interface PopupItem {
+  label: string;
+  checked?: boolean;
+  enabled?: boolean;
+  display?: boolean;
+  onClick?: Trigger;
+};
+
+type PopupMenu = ('separator' | PopupItem)[];
+
+type Cmap<A> = Map<string, A>
+type Cprops = ColProps<any>;
+type ColProps<R> = ColumnProps<R, any>;
+
+// --------------------------------------------------------------------------
+// --- Column Utilities
+// --------------------------------------------------------------------------
+
+const isVisible = (visible: Cmap<boolean>, col: Cprops) => {
+  const defaultVisible = col.visible ?? true;
+  switch (defaultVisible) {
+    case 'never': return false;
+    case 'always': return false;
+    default:
+      return visible.get(col.id) ?? defaultVisible;
+  }
+};
+
+const defaultGetter = (row: any, dataKey: string) => {
+  if (typeof row === 'object') return row[dataKey];
+  return undefined;
+};
+
+const defaultRenderer = (d: any) => (
+  <div className="dome-xTable-renderer dome-text-label">
+    {new String(d)}
+  </div>
+);
+
+function makeRowGetter<Key, Row>(model?: Model<Key, Row>) {
+  return ({ index }: Index) => model && model.getRowAt(index);
+};
+
+function makeDataGetter(
+  getter: ((row: any, dataKey: string) => any) = defaultGetter,
+  dataKey: string,
+): TableCellDataGetter {
+  return (({ rowData }) => {
+    try {
+      if (rowData !== undefined) return getter(rowData, dataKey);
+    } catch (err) {
+      console.error(
+        '[Dome.table] custom getter error',
+        'rowData:', rowData,
+        'dataKey:', dataKey,
+        err,
+      );
+    }
+    return undefined;
+  });
+}
+
+function makeDataRenderer(
+  render: ((data: any) => ReactNode) = defaultRenderer,
+  onContextMenu?: (row: any, index: number, dataKey: string) => void,
+): TableCellRenderer {
+  return (props => {
+    const cellData = props.cellData;
+    try {
+      const contents = cellData ? render(cellData) : null;
+      if (onContextMenu) {
+        const callback = (evt: React.MouseEvent) => {
+          evt.stopPropagation();
+          onContextMenu(props.rowData, props.rowIndex, props.dataKey);
+        };
+        return (<div onContextMenu={callback}>{contents}</div>);
+      }
+      return contents;
+    } catch (err) {
+      console.error(
+        '[Dome.table] custom renderer error',
+        'dataKey:', props.dataKey,
+        'cellData:', cellData,
+        err,
+      );
+      return null;
+    }
+  });
+}
+
+// --------------------------------------------------------------------------
+// --- Table Settings
+// --------------------------------------------------------------------------
+
+type ColSettings<A> = { [id: string]: undefined | null | A };
+
+type TableSettings = {
+  resize?: ColSettings<number>;
+  visible?: ColSettings<boolean>;
+}
+
+// --------------------------------------------------------------------------
+// --- Table State
+// --------------------------------------------------------------------------
+
+class TableState<Key, Row> {
+
+  settings?: string; // User settings
+  signal?: Trigger; // Full reload
+  width?: number; // Current table width
+  offset?: number; // Current resizing offset
+  resizing?: number; // Currently dragging resizer
+  resize: Cmap<number> = new Map(); // Current resizing wrt. dragging
+  visible: Cmap<boolean> = new Map(); // Current
+  headerRef: Cmap<divRef> = new Map(); // Once, build on demand
+  columnWith: Cmap<number> = new Map(); // DOM column element width without dragging
+  tableRef: tableRef = React.createRef(); // Once, global
+  getter: Cmap<TableCellDataGetter> = new Map(); // Computed from registry
+  render: Cmap<TableCellRenderer> = new Map(); // Computed from registry and getterFields
+  rowGetter: (info: Index) => Row | undefined; // Computed from last fetching
+  rendering?: RenderByFields<Row>; // Last user props used for computing renderers
+  model?: Model<Key, Row>; // Last user proxy used for computing getter
+  sorting?: Sorting; // Last user proxy used for sorting
+  client?: Client; // Client of last fetching
+  columns: ColProps<Row>[] = []; // Currently known columns
+  scrolledKey?: Key; // Lastly scrolled key
+  selectedIndex?: number; // Current selected index
+  sortBy?: string; // last sorting dataKey
+  sortDirection?: SortDirectionType; // last sorting direction
+  onContextMenu?: (row: Row, index: number) => void; // context menu callback
+  range?: IndexRange;
+  rowCount = 0;
+
+  constructor() {
+    this.unwind = this.unwind.bind(this);
+    this.forceUpdate = this.forceUpdate.bind(this);
+    this.fullReload = this.fullReload.bind(this);
+    this.updateGrid = this.updateGrid.bind(this);
+    this.onRowsRendered = this.onRowsRendered.bind(this);
+    this.rowClassName = this.rowClassName.bind(this);
+    this.onHeaderMenu = this.onHeaderMenu.bind(this);
+    this.onRowClick = this.onRowClick.bind(this);
+    this.onRowRightClick = this.onRowRightClick.bind(this);
+    this.onKeyDown = this.onKeyDown.bind(this);
+    this.onSorting = this.onSorting.bind(this);
+    this.clearSettings = this.clearSettings.bind(this);
+    this.rebuild = debounce(this.rebuild.bind(this), 5);
+    this.rowGetter = makeRowGetter();
+  }
+
+  // --- Static Callbacks
+
+  unwind() {
+    this.signal = undefined;
+    this.onSelection = undefined;
+    this.onContextMenu = undefined;
+  }
+
+  forceUpdate() {
+    const s = this.signal;
+    if (s) { this.signal = undefined; s(); }
+  }
+
+  updateGrid() {
+    this.tableRef.current?.forceUpdateGrid();
+  }
+
+  fullReload() {
+    this.scrolledKey = undefined;
+    this.forceUpdate();
+    this.updateGrid();
+  }
+
+  getRef(id: string) {
+    const href = this.headerRef.get(id);
+    if (href) return href;
+    const nref: divRef = React.createRef();
+    this.headerRef.set(id, nref);
+    return nref;
+  }
+
+  // --- Computing Column Size
+
+  computeWidth(id: string): number | undefined {
+    const cwidth = this.columnWith;
+    if (this.resizing !== undefined) return cwidth.get(id);
+    const elt = this.headerRef.get(id)?.current?.parentElement;
+    const cw = elt?.getBoundingClientRect()?.width;
+    if (cw) cwidth.set(id, cw);
+    return cw;
+  }
+
+  startResizing(idx: number) {
+    this.resizing = idx;
+    this.offset = 0;
+    this.forceUpdate();
+  }
+
+  stopResizing() {
+    this.resizing = undefined;
+    this.offset = undefined;
+    this.columnWith.clear();
+    this.updateSettings();
+  }
+
+  // Debounced
+  setResizeOffset(lcol: string, rcol: string, offset: number) {
+    const colws = this.columnWith;
+    const cwl = colws.get(lcol);
+    const cwr = colws.get(rcol);
+    const wl = cwl ? cwl + offset : 0;
+    const wr = cwr ? cwr - offset : 0;
+    if (wl > 40 && wr > 40) {
+      const resize = this.resize;
+      resize.set(lcol, wl);
+      resize.set(rcol, wr);
+      this.offset = offset;
+      this.forceUpdate(); // no settings yet, onStop only
+    }
+  }
+
+  // --- Table settings
+
+  clearSettings() {
+    this.resize.clear();
+    this.visible.clear();
+    this.forceUpdate();
+  }
+
+  updateSettings() {
+    const userSettings = this.settings;
+    if (userSettings) {
+      const cws: ColSettings<number> = {};
+      const cvs: ColSettings<boolean> = {};
+      const resize = this.resize;
+      const visible = this.visible;
+      this.columns.forEach(({ id }) => {
+        const cw = resize.get(id);
+        const cv = visible.get(id);
+        cws[id] = cw === undefined ? null : cw;
+        cvs[id] = cv === undefined ? null : cv;
+      });
+      const theSettings: TableSettings = { resize: cws, visible: cvs };
+      Dome.setWindowSetting(userSettings, theSettings);
+    }
+    this.forceUpdate();
+  }
+
+  importSettings(settings?: string) {
+    if (settings !== this.settings) {
+      this.settings = settings;
+      const resize = this.resize;
+      const visible = this.visible;
+      resize.clear();
+      visible.clear();
+      const theSettings: undefined | TableSettings =
+        Dome.getWindowSetting(settings);
+      if (theSettings) {
+        forEach(theSettings.resize, (cw, cid) => {
+          if (typeof cw === 'number') this.resize.set(cid, cw);
+        });
+        forEach(theSettings.visible, (cv, cid) => {
+          if (typeof cv === 'boolean') this.visible.set(cid, cv);
+        });
+        this.forceUpdate();
+      }
+    }
+  }
+
+  // --- User Table properties
+
+  setSorting(sorting?: Sorting) {
+    const info = sorting?.getSorting();
+    this.sortBy = info?.sortBy;
+    this.sortDirection = info?.sortDirection;
+    if (sorting !== this.sorting) {
+      this.sorting = sorting;
+      this.fullReload();
+    }
+  }
+
+  setModel(model?: Model<Key, Row>) {
+    if (model !== this.model) {
+      this.client?.unlink();
+      this.model = model;
+      if (model) {
+        const client = model.link();
+        client.onReload(this.fullReload);
+        client.onUpdate(this.updateGrid);
+        this.client = client;
+      } else {
+        this.client = undefined;
+      }
+      this.rowGetter = makeRowGetter(model);
+      this.fullReload();
+    }
+  }
+
+  setRendering(rendering?: RenderByFields<Row>) {
+    if (rendering !== this.rendering) {
+      this.rendering = rendering;
+      this.render.clear();
+      this.fullReload();
+    }
+  }
+
+  // ---- Selection Management
+
+  onSelection?: (data: Row, key: Key, index: number) => void;
+
+  onRowClick(info: RowMouseEventHandlerParams) {
+    const index = info.index;
+    const data = info.rowData as (Row | undefined);
+    const model = this.model;
+    const key = (data !== undefined) ? model?.getKeyFor(index, data) : undefined;
+    const onSelection = this.onSelection;
+    if (key !== undefined && data !== undefined && onSelection)
+      onSelection(data, key, index);
+  }
+
+  onRowsRendered(info: IndexRange) {
+    this.range = info;
+    this.client?.watch(info.startIndex, info.stopIndex);
+  }
+
+  rowClassName({ index }: Index): string {
+    if (this.selectedIndex === index) return 'dome-xTable-selected';
+    return (index & 1 ? 'dome-xTable-even' : 'dome-xTable-odd');
+  }
+
+  keyStepper(index: number) {
+    const onSelection = this.onSelection;
+    const key = this.model?.getKeyAt(index);
+    const data = this.model?.getRowAt(index);
+    if (key !== undefined && data !== undefined && onSelection) {
+      onSelection(data, key, index);
+    }
+  }
+
+  scrollToIndex(selection: Key | undefined): number | undefined {
+    const index = selection && this.model?.getIndexOf(selection);
+    this.selectedIndex = index;
+    if (this.scrolledKey !== selection) {
+      this.scrolledKey = selection;
+      if (selection) return index;
+    }
+    return undefined;
+  }
+
+  onSorting(ord?: SortingInfo) {
+    const sorting = this.sorting;
+    if (sorting) {
+      sorting.setSorting(ord);
+      this.sortBy = ord?.sortBy;
+      this.sortDirection = ord?.sortDirection;
+      this.forceUpdate();
+    }
+  }
+
+  // ---- Row Events
+
+  onRowRightClick({ event, rowData, index }: RowMouseEventHandlerParams) {
+    const callback = this.onContextMenu;
+    if (callback) {
+      event.stopPropagation();
+      callback(rowData, index);
+    }
+  }
+
+  onKeyDown(evt: React.KeyboardEvent) {
+    const index = this.selectedIndex;
+    switch (evt.key) {
+      case 'ArrowUp':
+        if (index !== undefined) {
+          this.keyStepper(index - 1);
+          evt.preventDefault();
+        }
+        break;
+      case 'ArrowDown':
+        if (index !== undefined) {
+          this.keyStepper(index + 1);
+          evt.preventDefault();
+        }
+        break;
+    }
+  }
+
+  // ---- Header Context Menu
+
+  onHeaderMenu() {
+    let has_order = false;
+    let has_resize = false;
+    let has_visible = false;
+    const visible = this.visible;
+    const columns = this.columns;
+    columns.forEach(col => {
+      if (!col.disableSort) has_order = true;
+      if (!col.fixed) has_resize = true;
+      if (col.visible !== 'never' && col.visible !== 'always')
+        has_visible = true;
+    });
+    const resetSizing = () => {
+      this.resize.clear();
+      this.updateSettings();
+    };
+    const resetColumns = () => {
+      this.visible.clear();
+      this.resize.clear();
+      this.updateSettings();
+    };
+    const items: PopupMenu = [
+      {
+        label: 'Reset ordering',
+        display: has_order && this.sorting,
+        onClick: this.onSorting,
+      },
+      {
+        label: 'Reset column widths',
+        display: has_resize,
+        onClick: resetSizing,
+      },
+      {
+        label: 'Restore column defaults',
+        display: has_visible,
+        onClick: resetColumns,
+      },
+      'separator',
+    ];
+    columns.forEach(col => {
+      switch (col.visible) {
+        case 'never':
+        case 'always':
+          break;
+        default:
+          const { id, label, title } = col;
+          const checked = isVisible(visible, col);
+          const onClick = () => {
+            visible.set(id, !checked);
+            this.updateSettings();
+          };
+          items.push({ label: label || title || id, checked, onClick });
+      }
+    });
+    Dome.popupMenu(items);
+  }
+
+  // --- Getter & Setters
+
+  computeGetter(id: string, dataKey: string, props: Cprops) {
+    const current = this.getter.get(id);
+    if (current) return current;
+    const dataGetter = makeDataGetter(props.getter, dataKey);
+    this.getter.set(id, dataGetter);
+    return dataGetter;
+  }
+
+  computeRender(id: string, dataKey: string, props: Cprops) {
+    const current = this.render.get(id);
+    if (current) return current;
+    let renderer = props.render;
+    if (!renderer) {
+      const rdr = this.rendering;
+      if (rdr) renderer = (rdr as any)[dataKey];
+    }
+    const cellRenderer = makeDataRenderer(renderer, props.onContextMenu);
+    this.render.set(id, cellRenderer);
+    return cellRenderer;
+  }
+
+  // --- User Column Registry
+
+  private registry = new Map<string, null | ColProps<Row>>();
+
+  setRegistry(id: string, props: null | ColProps<Row>) {
+    this.registry.set(id, props);
+    this.rebuild();
+  }
+
+  useColumn(props: ColProps<Row>): Trigger {
+    const id = props.id;
+    this.setRegistry(id, props);
+    return () => this.setRegistry(id, null);
+  }
+
+  rebuild() {
+    const current = this.columns;
+    const cols: ColProps<Row>[] = [];
+    this.registry.forEach((col) => col && cols.push(col));
+    if (!isEqual(current, cols)) {
+      this.getter.clear();
+      this.render.clear();
+      this.columns = cols;
+      this.fullReload();
+    }
+  }
+}
+
+// --------------------------------------------------------------------------
+// --- Columns Components
+// --------------------------------------------------------------------------
+
+const ColumnContext =
+  React.createContext<undefined | TableState<any, any>>(undefined);
+
+/**
+   Table Column.
+   @template Row - table row data of some table entries
+   @template Cell - type of cell data to render in this column
+ */
+export function Column<Row, Cell>(props: ColumnProps<Row, Cell>) {
+  const context = React.useContext(ColumnContext);
+  React.useEffect(() => context && context.useColumn(props));
+  return null;
+}
+
+// --------------------------------------------------------------------------
+// --- Virtualized Column
+// --------------------------------------------------------------------------
+
+function makeColumn<Key, Row>(
+  state: TableState<Key, Row>,
+  props: ColProps<Row>,
+  fill: boolean,
+) {
+  const { id } = props;
+  const align = { textAlign: props.align };
+  const dataKey = props.dataKey ?? id;
+  const columnData: ColumnData = {
+    icon: props.icon,
+    label: props.label,
+    title: props.title,
+    headerMenu: state.onHeaderMenu,
+    headerRef: state.getRef(id),
+  };
+  const width = state.resize.get(id) || props.width || 60;
+  const flexGrow = fill ? 1 : 0;
+  const sorting = state.sorting;
+  const disableSort =
+    props.disableSort || !sorting || !sorting.canSortBy(dataKey);
+  const getter = state.computeGetter(id, dataKey, props);
+  const render = state.computeRender(id, dataKey, props);
+  return (
+    <VColumn
+      key={id}
+      width={width}
+      flexGrow={flexGrow}
+      dataKey={dataKey}
+      columnData={columnData}
+      headerRenderer={headerRenderer}
+      cellDataGetter={getter}
+      cellRenderer={render}
+      headerStyle={align}
+      disableSort={disableSort}
+      style={align}
+    />
+  );
+};
+
+function makeCprops<Key, Row>(state: TableState<Key, Row>) {
+  const cols: Cprops[] = [];
+  state.columns.forEach((col) => {
+    if (col && isVisible(state.visible, col)) {
+      cols.push(col);
+    }
+  });
+  cols.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
+  return cols;
+}
+
+function makeColumns<Key, Row>(state: TableState<Key, Row>, cols: Cprops[]) {
+  let fill: undefined | Cprops;
+  let lastExt: undefined | Cprops;
+  cols.forEach((col) => {
+    if (col.fill && !fill) fill = col;
+    if (!col.fixed) lastExt = col;
+  });
+  const n = cols.length;
+  if (0 < n && !fill) fill = lastExt || cols[n - 1];
+  return cols.map((col) => makeColumn(state, col, col === fill));
+}
+
+// --------------------------------------------------------------------------
+// --- Table Utility Renderers
+// --------------------------------------------------------------------------
+
+const headerIcon = (icon?: string) => (
+  icon &&
+  (<div className='dome-xTable-header-icon'>
+    <SVG id={icon} />
+  </div>)
+);
+
+const headerLabel = (label?: string) => (
+  label &&
+  (<label className='dome-xTable-header-label dome-text-label'>
+    {label}
+  </label>)
+);
+
+const makeSorter = (id: string) => (
+  <div className='dome-xTable-header-sorter'>
+    <SVG id={id} size={8} />
+  </div>
+);
+
+const sorterASC = makeSorter('ANGLE.UP');
+const sorterDESC = makeSorter('ANGLE.DOWN');
+
+function headerRowRenderer(props: TableHeaderRowProps) {
+  return (
+    <div
+      role="row"
+      className={props.className}
+      style={props.style}
+    >
+      {props.columns}
+    </div>
+  );
+}
+
+function headerRenderer(props: TableHeaderProps) {
+  const data: ColumnData = props.columnData;
+  const { sortBy, sortDirection, dataKey } = props;
+  const { icon, label, title, headerRef, headerMenu } = data;
+  const sorter =
+    dataKey === sortBy
+      ? (sortDirection === SortDirection.ASC ? sorterASC : sorterDESC)
+      : undefined;
+  return (
+    <div
+      className='dome-xTable-header'
+      title={title}
+      ref={headerRef}
+      onContextMenu={headerMenu}
+    >
+      {headerIcon(icon)}
+      {headerLabel(label)}
+      {sorter}
+    </div>
+  );
+}
+
+// --------------------------------------------------------------------------
+// --- Column Resizer
+// --------------------------------------------------------------------------
+
+const DRAGGING = 'dome-xTable-resizer dome-color-dragging';
+const DRAGZONE = 'dome-xTable-resizer dome-color-dragzone';
+
+interface ResizerProps {
+  dragging: boolean; // Currently dragging
+  position: number; // drag-start offset
+  offset: number; // current offset
+  onStart: Trigger;
+  onStop: Trigger;
+  onDrag: (offset: number) => void;
+}
+
+const Resizer = (props: ResizerProps) => (
+  <DraggableCore
+    onStart={props.onStart}
+    onStop={props.onStop}
+    onDrag={(_elt, data) => props.onDrag(data.x - props.position)}
+  >
+    <div
+      className={props.dragging ? DRAGGING : DRAGZONE}
+      style={{ left: props.position + props.offset - 2 }}
+    />
+  </DraggableCore>
+);
+
+type ResizeInfo = { id: string, fixed: boolean, left?: string, right?: string };
+
+function makeResizers(
+  state: TableState<any, any>,
+  columns: Cprops[],
+): null | JSX.Element[] {
+  if (columns.length < 2) return null;
+  const resizing: ResizeInfo[] = columns.map(({ id, fixed = false }) => ({ id, fixed }));
+  var k: number, cid; // last non-fixed from left/right
+  for (cid = undefined, k = 0; k < columns.length; k++) {
+    const r = resizing[k];
+    r.left = cid;
+    if (!r.fixed) cid = r.id;
+  }
+  for (cid = undefined, k = columns.length - 1; 0 <= k; k--) {
+    const r = resizing[k];
+    r.right = cid;
+    if (!r.fixed) cid = r.id;
+  }
+  const cwidth = columns.map(col => state.computeWidth(col.id));
+  var position = 0, resizers = [];
+  for (k = 0; k < columns.length - 1; k++) {
+    const width = cwidth[k];
+    if (!width) return null;
+    position += width;
+    const a = resizing[k];
+    const b = resizing[k + 1];
+    if ((!a.fixed || !b.fixed) && a.right && b.left) {
+      const index = k; // Otherwize use dynamic value of k
+      const rcol = a.right;
+      const lcol = b.left;
+      const offset = state.offset ?? 0;
+      const dragging = state.resizing === index;
+      const onStart = () => state.startResizing(index);
+      const onStop = () => state.stopResizing();
+      const onDrag = (ofs: number) => state.setResizeOffset(lcol, rcol, ofs);
+      const resizer = (
+        <Resizer
+          key={index}
+          dragging={dragging}
+          position={position}
+          offset={offset}
+          onStart={onStart}
+          onStop={onStop}
+          onDrag={onDrag}
+        />
+      );
+      resizers.push(resizer);
+    }
+  }
+  return resizers;
+}
+
+// --------------------------------------------------------------------------
+// --- Virtualized Table View
+// --------------------------------------------------------------------------
+
+// Must be kept in sync with table.css
+const CSS_HEADER_HEIGHT = 22;
+const CSS_ROW_HEIGHT = 20;
+
+function makeTable<Key, Row>(
+  props: TableProps<Key, Row>,
+  state: TableState<Key, Row>,
+  size: Size,
+) {
+
+  const { width, height } = size;
+  const model = props.model;
+  const itemCount = model.getRowCount();
+  const tableHeight = CSS_HEADER_HEIGHT + CSS_ROW_HEIGHT * itemCount;
+  const smallHeight = itemCount > 0 && tableHeight < height;
+  const rowCount = (smallHeight ? itemCount + 1 : itemCount);
+  const scrollTo = state.scrollToIndex(props.selection);
+  const cprops = makeCprops(state);
+  const columns = makeColumns(state, cprops);
+  const resizers = makeResizers(state, cprops);
+
+  state.rowCount = rowCount;
+  if (state.width !== width) {
+    state.width = width;
+    setImmediate(state.forceUpdate);
+  }
+
+  return (
+    <div onKeyDown={state.onKeyDown}>
+      <VTable
+        ref={state.tableRef}
+        key="table"
+        displayName="React-Virtualized-Table"
+        width={width}
+        height={height}
+        rowCount={rowCount}
+        noRowsRenderer={props.renderEmpty}
+        rowGetter={state.rowGetter}
+        rowClassName={state.rowClassName}
+        rowHeight={CSS_ROW_HEIGHT}
+        headerHeight={CSS_HEADER_HEIGHT}
+        headerRowRenderer={headerRowRenderer}
+        onRowsRendered={state.onRowsRendered}
+        onRowClick={state.onRowClick}
+        onRowRightClick={state.onRowRightClick}
+        sortBy={state.sortBy}
+        sortDirection={state.sortDirection}
+        sort={state.onSorting}
+        scrollToIndex={scrollTo}
+        scrollToAlignment="auto"
+      >
+        {columns}
+      </VTable>
+      {resizers}
+    </div >
+  );
+};
+
+// --------------------------------------------------------------------------
+// --- Table View
+// --------------------------------------------------------------------------
+
+/** Table View.
+ 
+   This component is base on [React-Virtualized
+   Tables](https://bvaughn.github.io/react-virtualized/#/components/Table),
+   offering a lazy, super-optimized rendering process that scales on huge
+   datasets.
+ 
+   A table shall be connected to an instance of [[Model]] class to retrieve the
+   data and get informed of data updates.
+ 
+   The table children shall be instances of [[Column]] class, and can be grouped
+   into arbitrary level of React fragments or custom components.
+ 
+   Clicking on table headers trigger re-ordering callback on the model with the
+   expected column and direction, unless disabled _via_ the column x
+   specification. However, actual sorting (and corresponding feedback on table
+   headers) would only take place if the model supports re-ordering and
+   eventually triggers a reload.
+ 
+   Right-clicking the table headers displays a popup-menu with actions to reset
+   natural ordering, reset column widths and select column visibility.
+ 
+   Tables do not control item selection state. Instead, you shall supply the
+   selection state and callback _via_ properties, like any other controlled
+   React components.
+ 
+   Item selection can be based either on single-row or multiple-row. In case of
+   single-row selection (`multipleSelection:false`, the default), selection
+   state must be a single item or `undefined`, and the `onSelection` callback is
+   called with the same type of values.
+ 
+   In case of multiple-row selection (`multipleSelection:true`), the selection
+   state shall be an _array_ of items, and `onSelection` callback also. Single
+   items are _not_ accepted, but `undefined` selection can be used in place of
+   an empty array.
+ 
+   Clicking on a row triggers the `onSelection` callback with the updated
+   selection.  In single-selection mode, the clicked item is sent to the
+   callback. In multiple-selection mode, key modifiers are taken into account
+   for determining the new slection. By default, the new selection only contains
+   the clicked item. If the `Shift` modifier has been pressed, the current
+   selection is extended with a range of items from the last selected one, to
+   the newly selected one. If the `CtrlOrCmd` modifier has been pressed, the
+   selection is extended with the newly clicked item.  Clicking an already
+   selected item with the `CtrlOrCmd` modifier removes it from the current
+   selection.
+ 
+   @template Key - unique identifiers of table entries @template Row - data
+   associated to each key in the table entries */
+
+export function Table<Key, Row>(props: TableProps<Key, Row>) {
+
+  const state = React.useMemo(() => new TableState<Key, Row>(), []);
+  const [age, setAge] = React.useState(0);
+  React.useEffect(() => {
+    state.signal = () => setAge(age + 1);
+    state.importSettings(props.settings);
+    state.setModel(props.model);
+    state.setSorting(props.sorting);
+    state.setRendering(props.rendering);
+    state.onSelection = props.onSelection;
+    state.onContextMenu = props.onContextMenu;
+    return state.unwind;
+  });
+  Dome.useEvent('dome.defaults', state.clearSettings);
+
+  return (
+    <div className='dome-xTable'>
+      <ColumnContext.Provider value={state}>
+        {props.children}
+      </ColumnContext.Provider>
+      <AutoSizer key='table'>
+        {(size: Size) => makeTable(props, state, size)}
+      </AutoSizer>
+    </div>
+  );
+}
+
+// --------------------------------------------------------------------------
diff --git a/ivette/src/frama-c/states.ts b/ivette/src/frama-c/states.ts
index 94ccba06ede83ea5138782453bd45370a351962d..e0515681e67ad61d75bc699cc26c539f0f19abc6 100644
--- a/ivette/src/frama-c/states.ts
+++ b/ivette/src/frama-c/states.ts
@@ -178,7 +178,7 @@ export function useRequest(rq: string, params: any, options: any = {}) {
   const footprint = project ? JSON.stringify([project, rq, params]) : undefined;
 
   async function trigger() {
-    if (project && rq && params) {
+    if (project && rq && params !== undefined) {
       try {
         const r = await Server.GET({ endpoint: rq, params });
         setResponse(r);
diff --git a/ivette/src/renderer/Controller.tsx b/ivette/src/renderer/Controller.tsx
index 4fed24326a1c57f67b6f1f58c37ba84bea6c666c..f14fdadc91f84ee433a1d4ea56d33158d7395d3d 100644
--- a/ivette/src/renderer/Controller.tsx
+++ b/ivette/src/renderer/Controller.tsx
@@ -76,6 +76,10 @@ function buildServerConfig(argv: string[], cwd?: string) {
   };
 }
 
+function buildServerCommand(cmd: string) {
+  return buildServerConfig(cmd.trim().split(/[ \t\n]+/));
+}
+
 function insertConfig(hs: string[], cfg: Server.Configuration) {
   const cmd = dumpServerConfig(cfg).trim();
   const newhs =
@@ -90,8 +94,20 @@ function insertConfig(hs: string[], cfg: Server.Configuration) {
 // --- Start Server on Command
 // --------------------------------------------------------------------------
 
+let reloadCommand: string | undefined;
+
+Dome.onReload(() => {
+  const hst = Dome.getWindowSetting('Controller.history');
+  reloadCommand = Array.isArray(hst) && hst[0];
+});
+
 Dome.onCommand((argv: string[], cwd: string) => {
-  const cfg = buildServerConfig(argv, cwd);
+  let cfg;
+  if (reloadCommand) {
+    cfg = buildServerCommand(reloadCommand);
+  } else {
+    cfg = buildServerConfig(argv, cwd);
+  }
   Server.setConfig(cfg);
   Server.start();
 });
@@ -189,9 +205,7 @@ const RenderConsole = () => {
   };
 
   const doExec = () => {
-    const cmd = editor.getValue().trim();
-    const argv = cmd.split(/[ \t\n]+/);
-    const cfg = buildServerConfig(argv);
+    const cfg = buildServerCommand(editor.getValue());
     const hst = insertConfig(history, cfg);
     setHistory(hst);
     setCursor(-1);
diff --git a/ivette/src/renderer/Properties.tsx b/ivette/src/renderer/Properties.tsx
index c17bfe7f81d295327cbd6fd30da2ab7f6b5e5f3f..3061204a39bbad89fbd9a1b5213c57263769514e 100644
--- a/ivette/src/renderer/Properties.tsx
+++ b/ivette/src/renderer/Properties.tsx
@@ -5,68 +5,134 @@
 import _ from 'lodash';
 import React from 'react';
 import * as States from 'frama-c/states';
+import * as Compare from 'dome/data/compare';
 import { Label, Code } from 'dome/controls/labels';
 import { ArrayModel } from 'dome/table/arrays';
-import { Table, DefineColumn } from 'dome/table/views';
+import { Table, Column, ColumnProps, Renderer } from 'dome/table/views';
 import { Component } from 'frama-c/LabViews';
 
 // --------------------------------------------------------------------------
 // --- Property Columns
 // --------------------------------------------------------------------------
 
-const ColumnCode: any =
-  DefineColumn({ renderValue: (text: string) => <Code>{text}</Code> });
+export const renderCode: Renderer<string> =
+  (text?: string) => (text ? <Code>{text}</Code> : null);
 
-const ColumnTag: any =
-  DefineColumn({
-    renderValue: (l: { label: string; descr: string }) => (
-      <Label label={l.label} title={l.descr} />
-    ),
-  });
+function ColumnCode<Row>(props: ColumnProps<Row, string>) {
+  return <Column render={renderCode} {...props} />;
+}
+
+interface Tag { name: string; label: string; descr: string }
+
+export const renderTag: Renderer<Tag> =
+  (d?: Tag) => (d ? <Label label={d.label} title={d.descr} /> : null);
+
+function ColumnTag<Row>(props: ColumnProps<Row, Tag>) {
+  return <Column render={renderTag} {...props} />;
+}
 
 // --------------------------------------------------------------------------
 // --- Properties Table
 // -------------------------------------------------------------------------
 
+interface SourceLoc {
+  file: string;
+  line: number;
+}
+
+interface Property {
+  key: string;
+  descr: string;
+  kind: string;
+  status: string;
+  function?: string;
+  kinstr: string;
+  source: SourceLoc;
+}
+
+const bySource =
+  Compare.byFields<SourceLoc>({ file: Compare.alpha, line: Compare.primitive });
+
+const byStatus =
+  Compare.byRank(
+    'inconsistent',
+    'invalid',
+    'invalid_under_hyp',
+    'unknown',
+    'valid_under_hyp',
+    'valid',
+    'invalid_but_dead',
+    'unknown_but_dead',
+    'valid_but_dead',
+    'never_tried',
+    'considered_valid',
+  );
+
+const byProperty: Compare.ByFields<Property> = {
+  status: byStatus,
+  function: Compare.defined(Compare.alpha),
+  source: bySource,
+  kind: Compare.primitive,
+  key: Compare.primitive,
+  kinstr: Compare.primitive,
+};
+
+class PropertyModel extends ArrayModel<Property> {
+  constructor() {
+    super('key');
+    this.setOrderingByFields(byProperty);
+  }
+}
+
 const RenderTable = () => {
   // Hooks
-  const model = React.useMemo(() => new ArrayModel(), []);
-  const items = States.useSyncArray('kernel.properties');
-  const status = States.useDictionary('kernel.dictionary.propstatus');
-  const [select, setSelect] = States.useSelection();
+  const model = React.useMemo(() => new PropertyModel(), []);
+  const items: { [key: string]: Property } =
+    States.useSyncArray('kernel.properties');
+  const statusDict: { [status: string]: Tag } =
+    States.useDictionary('kernel.dictionary.propstatus');
+  const [select, setSelect] =
+    States.useSelection();
+
   React.useEffect(() => {
-    model.setData(_.toArray(items));
+    const data = _.toArray(items);
+    model.replace(data);
   }, [model, items]);
 
   // Callbacks
-  const getStatus = ({ status: st }: any) => status[st] || { label: st };
-  const selection = select ? items[select.marker] : undefined;
-  const onSelection = (item: any) => item && setSelect({
-    marker: item.key,
-    function: item.function,
-  });
+  const getStatus = React.useCallback(
+    ({ status: st }: Property) => (statusDict[st] ?? { label: st }),
+    [statusDict],
+  );
+
+  const onSelection = React.useCallback(
+    ({ key, function: fct }: Property) => {
+      setSelect({ marker: key, function: fct });
+    }, [setSelect],
+  );
+
+  const selection = select?.marker;
 
   // Rendering
   return (
-    <>
-      <Table
-        model={model}
-        selection={selection}
-        onSelection={onSelection}
-        scrollToItem={selection}
-      >
-        <ColumnCode id="function" label="Function" width={120} />
-        <ColumnCode id="descr" label="Description" fill />
-        <ColumnTag
-          id="status"
-          label="Status"
-          fixed
-          width={80}
-          align="center"
-          getValue={getStatus}
-        />
-      </Table>
-    </>
+    <Table<string, Property>
+      model={model}
+      sorting={model}
+      selection={selection}
+      onSelection={onSelection}
+      settings="ivette.properties.table"
+    >
+      <ColumnCode id="function" label="Function" width={120} />
+      <ColumnCode id="descr" label="Description" fill />
+      <ColumnTag
+        id="status"
+        label="Status"
+        fixed
+        width={80}
+        align="center"
+        getter={getStatus}
+      />
+    </Table>
   );
 };
 
diff --git a/ivette/yarn.lock b/ivette/yarn.lock
index 26425e68bbe7e3614e4d8c1ebfc3315a0ab914f1..7be39fad855443cb9b03db8b55b67c6a0b192ecf 100644
--- a/ivette/yarn.lock
+++ b/ivette/yarn.lock
@@ -1081,6 +1081,14 @@
   dependencies:
     "@types/react" "*"
 
+"@types/react-virtualized@^9.21.10":
+  version "9.21.10"
+  resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.10.tgz#cd072dc9c889291ace2c4c9de8e8c050da8738b7"
+  integrity sha512-f5Ti3A7gGdLkPPFNHTrvKblpsPNBiQoSorOEOD+JPx72g/Ng2lOt4MYfhvQFQNgyIrAro+Z643jbcKafsMW2ag==
+  dependencies:
+    "@types/prop-types" "*"
+    "@types/react" "*"
+
 "@types/react@*", "@types/react@^16.9.17":
   version "16.9.32"
   resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.32.tgz#f6368625b224604148d1ddf5920e4fefbd98d383"
@@ -7093,6 +7101,11 @@ react-draggable@^4.2.0:
     classnames "^2.2.5"
     prop-types "^15.6.0"
 
+react-fast-compare@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
+  integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
+
 react-hot-loader@^4.12.20:
   version "4.12.20"
   resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.20.tgz#c2c42362a7578e5c30357a5ff7afa680aa0bef8a"