From 784de8fe771d9104800aa64133ecf87be019cb4c Mon Sep 17 00:00:00 2001 From: dmail Date: Mon, 2 Mar 2026 17:13:10 +0100 Subject: [PATCH 01/25] Prepare updating routes --- packages/frontend/navi/src/nav/route.js | 5 +++ packages/frontend/navi/src/nav/route.jsx | 3 +- .../src/client/database/database_routes.jsx | 7 ++-- .../src/client/main_routes.jsx | 5 ++- .../src/client/role/role_routes.jsx | 5 ++- .../src/client/routes.js | 32 +++++++++---------- .../src/client/table/table_routes.jsx | 5 ++- 7 files changed, 37 insertions(+), 25 deletions(-) diff --git a/packages/frontend/navi/src/nav/route.js b/packages/frontend/navi/src/nav/route.js index af9e4d848d..a195a56822 100644 --- a/packages/frontend/navi/src/nav/route.js +++ b/packages/frontend/navi/src/nav/route.js @@ -485,6 +485,11 @@ export const updateRoutes = ( }; const registerRoute = (routePattern) => { + if (typeof routePattern !== "string") { + throw new TypeError( + `registerRoute() expects a route pattern string, but received ${routePattern}.`, + ); + } const urlPatternRaw = routePattern.originalPattern; if (DEBUG) { console.debug(`Creating route: ${urlPatternRaw}`); diff --git a/packages/frontend/navi/src/nav/route.jsx b/packages/frontend/navi/src/nav/route.jsx index de157f9f19..a24ab96bee 100644 --- a/packages/frontend/navi/src/nav/route.jsx +++ b/packages/frontend/navi/src/nav/route.jsx @@ -6,7 +6,8 @@ * . Tester le code splitting avec .lazy + import dynamique * pour les elements des routes * - * 3. Ajouter la possibilite d'avoir des action sur les routes + * 3. Ajouter la possibilite d'avoir des + * sur les routes * Tester juste les data pour commencer * On aura ptet besoin d'un useRouteData au lieu de passer par un element qui est une fonction * pour que react ne re-render pas tout diff --git a/packages/related/plugin-database-manager/src/client/database/database_routes.jsx b/packages/related/plugin-database-manager/src/client/database/database_routes.jsx index 595348d3af..ebb935887b 100644 --- a/packages/related/plugin-database-manager/src/client/database/database_routes.jsx +++ b/packages/related/plugin-database-manager/src/client/database/database_routes.jsx @@ -4,8 +4,9 @@ import { DatabasePage } from "./database_page.jsx"; export const DatabaseRoutes = () => { return ( - - {(database) => } - + } + /> ); }; diff --git a/packages/related/plugin-database-manager/src/client/main_routes.jsx b/packages/related/plugin-database-manager/src/client/main_routes.jsx index bb93640656..9aebb3b804 100644 --- a/packages/related/plugin-database-manager/src/client/main_routes.jsx +++ b/packages/related/plugin-database-manager/src/client/main_routes.jsx @@ -1,4 +1,3 @@ -import { UITransition } from "@jsenv/navi"; import { DatabaseRoutes } from "./database/database_routes.jsx"; import "./database_manager.css" with { type: "css" }; import "./layout/layout.css" with { type: "css" }; @@ -8,10 +7,10 @@ import { TableRoutes } from "./table/table_routes.jsx"; export const MainRoutes = () => { return ( - + <> - + ); }; diff --git a/packages/related/plugin-database-manager/src/client/role/role_routes.jsx b/packages/related/plugin-database-manager/src/client/role/role_routes.jsx index bb690d8c80..fba482f783 100644 --- a/packages/related/plugin-database-manager/src/client/role/role_routes.jsx +++ b/packages/related/plugin-database-manager/src/client/role/role_routes.jsx @@ -1,7 +1,10 @@ import { Route } from "@jsenv/navi"; + import { ROLE_ROUTE } from "../routes.js"; import { RolePage } from "./role_page.jsx"; export const RoleRoutes = () => { - return {(role) => }; + return ( + } /> + ); }; diff --git a/packages/related/plugin-database-manager/src/client/routes.js b/packages/related/plugin-database-manager/src/client/routes.js index d05e402222..dcb1057734 100644 --- a/packages/related/plugin-database-manager/src/client/routes.js +++ b/packages/related/plugin-database-manager/src/client/routes.js @@ -5,26 +5,26 @@ import { TABLE, TABLE_ROW } from "./table/table_store.js"; setBaseUrl(window.DB_MANAGER_CONFIG.pathname); -let [ +export const [ ROLE_ROUTE, DATABASE_ROUTE, TABLE_ROUTE, - TABLE_DATA_ROUTE, + TABLE_ROW_ROUTE, TABLE_SETTINGS_ROUTE, ] = setupRoutes({ - "/roles/:rolname": ROLE.GET, - "/databases/:datname": DATABASE.GET, - "/tables/:tablename/*?": TABLE.GET, - "/tables/:tablename": TABLE_ROW.GET_MANY, - "/tables/:tablename/settings": createAction(() => {}, { - name: "get table settings", - }), + ROLE_ROUTE: "/roles/:rolname", + DATABASE_ROUTE: "/databases/:datname", + TABLE_ROUTE: "/tables/:tablename/", + TABLE_ROW_ROUTE: "/tables/:tablename", + TABLE_SETTINGS_ROUTE: "/tables/:tablename/settings", }); -export { - DATABASE_ROUTE, - ROLE_ROUTE, - TABLE_DATA_ROUTE, - TABLE_ROUTE, - TABLE_SETTINGS_ROUTE, -}; +ROLE_ROUTE.bindAction(ROLE.GET); +DATABASE_ROUTE.bindAction(DATABASE.GET); +TABLE_ROUTE.bindAction(TABLE.GET); +TABLE_ROW_ROUTE.bindAction(TABLE_ROW.GET_MANY); +TABLE_SETTINGS_ROUTE.bindAction( + createAction(() => {}, { + name: "get table settings", + }), +); diff --git a/packages/related/plugin-database-manager/src/client/table/table_routes.jsx b/packages/related/plugin-database-manager/src/client/table/table_routes.jsx index f74d7ecbe9..f65ba5457b 100644 --- a/packages/related/plugin-database-manager/src/client/table/table_routes.jsx +++ b/packages/related/plugin-database-manager/src/client/table/table_routes.jsx @@ -4,6 +4,9 @@ import { TablePage } from "./table_page.jsx"; export const TableRoutes = () => { return ( - {(table) => } + } + /> ); }; From 221c225adc93a66f756cac05163623a8f1315ad8 Mon Sep 17 00:00:00 2001 From: dmail Date: Mon, 2 Mar 2026 17:20:56 +0100 Subject: [PATCH 02/25] wokr --- packages/frontend/navi/src/nav/route.js | 5 ----- .../frontend/navi/src/nav/route_pattern.js | 6 ++++++ .../src/client/routes.js | 11 +++++----- .../src/client/table/table_page.jsx | 20 ++++++++++--------- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/frontend/navi/src/nav/route.js b/packages/frontend/navi/src/nav/route.js index a195a56822..af9e4d848d 100644 --- a/packages/frontend/navi/src/nav/route.js +++ b/packages/frontend/navi/src/nav/route.js @@ -485,11 +485,6 @@ export const updateRoutes = ( }; const registerRoute = (routePattern) => { - if (typeof routePattern !== "string") { - throw new TypeError( - `registerRoute() expects a route pattern string, but received ${routePattern}.`, - ); - } const urlPatternRaw = routePattern.originalPattern; if (DEBUG) { console.debug(`Creating route: ${urlPatternRaw}`); diff --git a/packages/frontend/navi/src/nav/route_pattern.js b/packages/frontend/navi/src/nav/route_pattern.js index 29a55a08e3..ea3d78bd47 100644 --- a/packages/frontend/navi/src/nav/route_pattern.js +++ b/packages/frontend/navi/src/nav/route_pattern.js @@ -2580,6 +2580,12 @@ export const setupPatterns = (patternDefinitions) => { // Phase 1: Create all pattern objects for (const [key, urlPatternRaw] of Object.entries(patternDefinitions)) { + if (typeof urlPatternRaw !== "string") { + throw new TypeError( + `expects a route pattern string, but received ${urlPatternRaw} for route "${key}".`, + ); + } + // Create the unified pattern object const pattern = createRoutePattern(urlPatternRaw); diff --git a/packages/related/plugin-database-manager/src/client/routes.js b/packages/related/plugin-database-manager/src/client/routes.js index dcb1057734..1adb4686b7 100644 --- a/packages/related/plugin-database-manager/src/client/routes.js +++ b/packages/related/plugin-database-manager/src/client/routes.js @@ -1,28 +1,29 @@ import { createAction, setBaseUrl, setupRoutes } from "@jsenv/navi"; + import { DATABASE } from "./database/database_store.js"; import { ROLE } from "./role/role_store.js"; import { TABLE, TABLE_ROW } from "./table/table_store.js"; setBaseUrl(window.DB_MANAGER_CONFIG.pathname); -export const [ +export const { ROLE_ROUTE, DATABASE_ROUTE, TABLE_ROUTE, - TABLE_ROW_ROUTE, + TABLE_DATA_ROUTE, TABLE_SETTINGS_ROUTE, -] = setupRoutes({ +} = setupRoutes({ ROLE_ROUTE: "/roles/:rolname", DATABASE_ROUTE: "/databases/:datname", TABLE_ROUTE: "/tables/:tablename/", - TABLE_ROW_ROUTE: "/tables/:tablename", + TABLE_DATA_ROUTE: "/tables/:tablename", TABLE_SETTINGS_ROUTE: "/tables/:tablename/settings", }); ROLE_ROUTE.bindAction(ROLE.GET); DATABASE_ROUTE.bindAction(DATABASE.GET); TABLE_ROUTE.bindAction(TABLE.GET); -TABLE_ROW_ROUTE.bindAction(TABLE_ROW.GET_MANY); +TABLE_DATA_ROUTE.bindAction(TABLE_ROW.GET_MANY); TABLE_SETTINGS_ROUTE.bindAction( createAction(() => {}, { name: "get table settings", diff --git a/packages/related/plugin-database-manager/src/client/table/table_page.jsx b/packages/related/plugin-database-manager/src/client/table/table_page.jsx index c99b35ddd4..97e9397fa6 100644 --- a/packages/related/plugin-database-manager/src/client/table/table_page.jsx +++ b/packages/related/plugin-database-manager/src/client/table/table_page.jsx @@ -18,7 +18,7 @@ * */ -import { Route, Tab, TabList, UITransition, useRouteStatus } from "@jsenv/navi"; +import { Route, Routes, Tab, TabList, useRouteStatus } from "@jsenv/navi"; import { Page, PageBody, PageHead } from "../layout/page.jsx"; import { TABLE_DATA_ROUTE, TABLE_SETTINGS_ROUTE } from "../routes.js"; import { DataSvg } from "../svg/data_svg.jsx"; @@ -66,14 +66,16 @@ export const TablePage = ({ table }) => { - - - {(rows) => } - - - {() => } - - + + } + /> + } + /> + ); From 27a45405052de69d04a55009b173143d1314cd4c Mon Sep 17 00:00:00 2001 From: dmail Date: Mon, 2 Mar 2026 17:41:54 +0100 Subject: [PATCH 03/25] Prepare a route action status --- .../src/action/action_private_properties.js | 16 + packages/frontend/navi/src/nav/route.js | 500 ++++++++++-------- 2 files changed, 282 insertions(+), 234 deletions(-) diff --git a/packages/frontend/navi/src/action/action_private_properties.js b/packages/frontend/navi/src/action/action_private_properties.js index 7de1757f3f..471c88f867 100644 --- a/packages/frontend/navi/src/action/action_private_properties.js +++ b/packages/frontend/navi/src/action/action_private_properties.js @@ -1,3 +1,5 @@ +import { ABORTED, COMPLETED, IDLE, RUNNING } from "./action_run_states.js"; + const actionPrivatePropertiesWeakMap = new WeakMap(); export const getActionPrivateProperties = (action) => { const actionPrivateProperties = actionPrivatePropertiesWeakMap.get(action); @@ -9,3 +11,17 @@ export const getActionPrivateProperties = (action) => { export const setActionPrivateProperties = (action, properties) => { actionPrivatePropertiesWeakMap.set(action, properties); }; + +export const getActionStatus = (action) => { + const { runningStateSignal, errorSignal, computedDataSignal } = + getActionPrivateProperties(action); + const runningState = runningStateSignal.value; + const idle = runningState === IDLE; + const aborted = runningState === ABORTED; + const error = errorSignal.value; + const loading = runningState === RUNNING; + const completed = runningState === COMPLETED; + const data = computedDataSignal.value; + + return { idle, loading, aborted, error, completed, data }; +}; diff --git a/packages/frontend/navi/src/nav/route.js b/packages/frontend/navi/src/nav/route.js index af9e4d848d..51bf8964cc 100644 --- a/packages/frontend/navi/src/nav/route.js +++ b/packages/frontend/navi/src/nav/route.js @@ -6,6 +6,7 @@ import { createPubSub } from "@jsenv/dom"; import { batch, computed, effect, signal } from "@preact/signals"; +import { getActionStatus } from "../action/action_private_properties.js"; import { compareTwoJsValues } from "../utils/compare_two_js_values.js"; import { resolveRouteUrl, setupPatterns } from "./route_pattern.js"; @@ -91,17 +92,29 @@ export const useRouteStatus = (route) => { `useRouteStatus() requires a route object, but received ${route}.`, ); } - const { urlSignal, matchingSignal, paramsSignal, visitedSignal } = route; + const { + urlSignal, + matchingSignal, + paramsSignal, + visitedSignal, + actionStatusSignal, + } = route; const url = urlSignal.value; const matching = matchingSignal.value; const params = paramsSignal.value; const visited = visitedSignal.value; + const { loading, aborted, error, completed, data } = actionStatusSignal.value; return { url, matching, params, visited, + loading, + aborted, + error, + completed, + data, }; }; @@ -153,6 +166,8 @@ export const updateRoutes = ( // state } = {}, ) => { + const returnValue = {}; + const routeMatchInfoSet = new Set(); for (const route of routeSet) { const routePrivateProperties = getRoutePrivateProperties(route); @@ -200,288 +215,292 @@ export const updateRoutes = ( }); } - // Apply all signal updates in a batch - const matchingRouteSet = new Set(); - batch(() => { - for (const { - route, - routePrivateProperties, - newMatching, - newParams, - } of routeMatchInfoSet) { - const { updateStatus } = routePrivateProperties; - const visited = isVisited(route.url); - updateStatus({ - matching: newMatching, - params: newParams, - visited, - }); - if (newMatching) { - matchingRouteSet.add(route); - } - } - + sync_routes_with_url: { // URL -> Signal synchronization (moved from individual route effects to eliminate circular dependency) // Prevent signal-to-URL synchronization during URL-to-signal synchronization isUpdatingRoutesFromUrl = true; + // Apply all signal updates in a batch + const matchingRouteSet = new Set(); + batch(() => { + for (const { + route, + routePrivateProperties, + newMatching, + newParams, + } of routeMatchInfoSet) { + const { updateStatus } = routePrivateProperties; + const visited = isVisited(route.url); + updateStatus({ + matching: newMatching, + params: newParams, + visited, + }); + if (newMatching) { + matchingRouteSet.add(route); + } + } - for (const { - route, - routePrivateProperties, - newMatching, - } of routeMatchInfoSet) { - const { routePattern } = routePrivateProperties; - const { connectionMap } = routePattern; - - for (const [paramName, connection] of connectionMap) { - const { signal: paramSignal, debug } = connection; - const rawParams = route.rawParamsSignal.value; - const urlParamValue = rawParams[paramName]; - - if (!newMatching) { - // Route doesn't match - check if any matching route extracts this parameter - let parameterExtractedByMatchingRoute = false; - let matchingRouteInSameFamily = false; + for (const { + route, + routePrivateProperties, + newMatching, + } of routeMatchInfoSet) { + const { routePattern } = routePrivateProperties; + const { connectionMap } = routePattern; + + for (const [paramName, connection] of connectionMap) { + const { signal: paramSignal, debug } = connection; + const rawParams = route.rawParamsSignal.value; + const urlParamValue = rawParams[paramName]; + + if (!newMatching) { + // Route doesn't match - check if any matching route extracts this parameter + let parameterExtractedByMatchingRoute = false; + let matchingRouteInSameFamily = false; + + for (const otherRoute of routeSet) { + if (otherRoute === route || !otherRoute.matching) { + continue; + } + const otherRawParams = otherRoute.rawParamsSignal.value; + const otherRoutePrivateProperties = + getRoutePrivateProperties(otherRoute); - for (const otherRoute of routeSet) { - if (otherRoute === route || !otherRoute.matching) { - continue; - } - const otherRawParams = otherRoute.rawParamsSignal.value; - const otherRoutePrivateProperties = - getRoutePrivateProperties(otherRoute); + // Check if this matching route extracts the parameter + if (paramName in otherRawParams) { + parameterExtractedByMatchingRoute = true; + } - // Check if this matching route extracts the parameter - if (paramName in otherRawParams) { - parameterExtractedByMatchingRoute = true; - } + // Check if this matching route is in the same family using parent-child relationships + const thisPatternObj = routePattern; + const otherPatternObj = otherRoutePrivateProperties.routePattern; - // Check if this matching route is in the same family using parent-child relationships - const thisPatternObj = routePattern; - const otherPatternObj = otherRoutePrivateProperties.routePattern; - - // Routes are in same family if they share a hierarchical relationship: - // 1. One is parent/ancestor of the other - // 2. They share a common parent/ancestor - let inSameFamily = false; - - // Check if other route is ancestor of this route - let currentParent = thisPatternObj.parent; - while (currentParent) { - if (currentParent === otherPatternObj) { - inSameFamily = true; - break; - } - currentParent = currentParent.parent; - } + // Routes are in same family if they share a hierarchical relationship: + // 1. One is parent/ancestor of the other + // 2. They share a common parent/ancestor + let inSameFamily = false; - // Check if this route is ancestor of other route - if (!inSameFamily) { - currentParent = otherPatternObj.parent; + // Check if other route is ancestor of this route + let currentParent = thisPatternObj.parent; while (currentParent) { - if (currentParent === thisPatternObj) { + if (currentParent === otherPatternObj) { inSameFamily = true; break; } currentParent = currentParent.parent; } - } - // Check if they share a common parent (siblings or cousins) - if (!inSameFamily) { - const thisAncestors = new Set(); - currentParent = thisPatternObj.parent; - while (currentParent) { - thisAncestors.add(currentParent); - currentParent = currentParent.parent; + // Check if this route is ancestor of other route + if (!inSameFamily) { + currentParent = otherPatternObj.parent; + while (currentParent) { + if (currentParent === thisPatternObj) { + inSameFamily = true; + break; + } + currentParent = currentParent.parent; + } } - currentParent = otherPatternObj.parent; - while (currentParent) { - if (thisAncestors.has(currentParent)) { - inSameFamily = true; - break; + // Check if they share a common parent (siblings or cousins) + if (!inSameFamily) { + const thisAncestors = new Set(); + currentParent = thisPatternObj.parent; + while (currentParent) { + thisAncestors.add(currentParent); + currentParent = currentParent.parent; + } + + currentParent = otherPatternObj.parent; + while (currentParent) { + if (thisAncestors.has(currentParent)) { + inSameFamily = true; + break; + } + currentParent = currentParent.parent; } - currentParent = currentParent.parent; } - } - if (inSameFamily) { - matchingRouteInSameFamily = true; + if (inSameFamily) { + matchingRouteInSameFamily = true; + } } - } - // Only reset signal if: - // 1. We're navigating within the same route family (not to completely unrelated routes) - // 2. AND no matching route extracts this parameter from URL - // 3. AND parameter has no default value (making it truly optional) - if (matchingRouteInSameFamily && !parameterExtractedByMatchingRoute) { - const defaultValue = connection.getDefaultValue(); - if (defaultValue === undefined) { - // Parameter is not extracted within same family and has no default - reset it - if (debug) { + // Only reset signal if: + // 1. We're navigating within the same route family (not to completely unrelated routes) + // 2. AND no matching route extracts this parameter from URL + // 3. AND parameter has no default value (making it truly optional) + if ( + matchingRouteInSameFamily && + !parameterExtractedByMatchingRoute + ) { + const defaultValue = connection.getDefaultValue(); + if (defaultValue === undefined) { + // Parameter is not extracted within same family and has no default - reset it + if (debug) { + console.debug( + `[route] Same family navigation, ${paramName} not extracted and has no default: resetting signal`, + ); + } + paramSignal.value = undefined; + } else if (debug) { + // Parameter has a default value - preserve current signal value console.debug( - `[route] Same family navigation, ${paramName} not extracted and has no default: resetting signal`, + `[route] Parameter ${paramName} has default value ${defaultValue}: preserving signal value: ${paramSignal.value}`, ); } - paramSignal.value = undefined; } else if (debug) { - // Parameter has a default value - preserve current signal value - console.debug( - `[route] Parameter ${paramName} has default value ${defaultValue}: preserving signal value: ${paramSignal.value}`, - ); + if (!matchingRouteInSameFamily) { + console.debug( + `[route] Different route family: preserving ${paramName} signal value: ${paramSignal.value}`, + ); + } else { + console.debug( + `[route] Parameter ${paramName} extracted by matching route: preserving signal value: ${paramSignal.value}`, + ); + } } - } else if (debug) { - if (!matchingRouteInSameFamily) { - console.debug( - `[route] Different route family: preserving ${paramName} signal value: ${paramSignal.value}`, - ); - } else { + continue; + } + + // URL -> Signal sync: When route matches, ensure signal matches URL state + // URL is the source of truth for explicit parameters + const value = paramSignal.peek(); + if (urlParamValue === undefined) { + // No URL parameter - reset signal to its current default value + // (handles both static fallback and dynamic default cases) + const defaultValue = connection.getDefaultValue(); + if (connection.isDefaultValue(value)) { + // Signal already has correct default value, no sync needed + continue; + } + if (debug) { console.debug( - `[route] Parameter ${paramName} extracted by matching route: preserving signal value: ${paramSignal.value}`, + `[route] URL->Signal: ${paramName} not in URL, reset signal to default (${defaultValue})`, ); } + paramSignal.value = defaultValue; + continue; } - continue; - } - - // URL -> Signal sync: When route matches, ensure signal matches URL state - // URL is the source of truth for explicit parameters - const value = paramSignal.peek(); - if (urlParamValue === undefined) { - // No URL parameter - reset signal to its current default value - // (handles both static fallback and dynamic default cases) - const defaultValue = connection.getDefaultValue(); - if (connection.isDefaultValue(value)) { - // Signal already has correct default value, no sync needed + if (urlParamValue === value) { + // Values already match, no sync needed continue; } if (debug) { console.debug( - `[route] URL->Signal: ${paramName} not in URL, reset signal to default (${defaultValue})`, + `[route] URL->Signal: ${paramName}=${urlParamValue} in url, sync signal with url`, ); } - paramSignal.value = defaultValue; + paramSignal.value = urlParamValue; continue; } - if (urlParamValue === value) { - // Values already match, no sync needed - continue; + } + }); + // Reset flag after URL -> Signal synchronization is complete + isUpdatingRoutesFromUrl = false; + + Object.assign(returnValue, { matchingRouteSet }); + } + + update_route_actions: { + // must be after paramsSignal.value update to ensure the proxy target is set + // (so after the batch call) + const toLoadSet = new Set(); + const toReloadSet = new Set(); + const abortSignalMap = new Map(); + const routeLoadRequestedMap = new Map(); + const shouldLoadOrReload = (route, shouldLoad) => { + const routeAction = route.action; + const currentAction = routeAction.getCurrentAction(); + if (shouldLoad) { + if ( + navigationType === "replace" || + currentAction.aborted || + currentAction.error + ) { + shouldLoad = false; } - if (debug) { + } + if (shouldLoad) { + toLoadSet.add(currentAction); + } else { + toReloadSet.add(currentAction); + } + routeLoadRequestedMap.set(route, currentAction); + // Create a new abort controller for this action + const actionAbortController = new AbortController(); + actionAbortControllerWeakMap.set(currentAction, actionAbortController); + abortSignalMap.set(currentAction, actionAbortController.signal); + }; + const shouldLoad = (route) => { + shouldLoadOrReload(route, true); + }; + const shouldReload = (route) => { + shouldLoadOrReload(route, false); + }; + const shouldAbort = (route) => { + const routeAction = route.action; + const currentAction = routeAction.getCurrentAction(); + const actionAbortController = + actionAbortControllerWeakMap.get(currentAction); + if (actionAbortController) { + actionAbortController.abort(`route no longer matching`); + actionAbortControllerWeakMap.delete(currentAction); + } + }; + for (const { + route, + routePrivateProperties, + newMatching, + oldMatching, + newParams, + oldParams, + } of routeMatchInfoSet) { + const routeAction = route.action; + if (!routeAction) { + continue; + } + + const becomesMatching = newMatching && !oldMatching; + const becomesNotMatching = !newMatching && oldMatching; + const paramsChangedWhileMatching = + newMatching && oldMatching && newParams !== oldParams; + + // Handle actions for routes that become matching + if (becomesMatching) { + if (DEBUG) { console.debug( - `[route] URL->Signal: ${paramName}=${urlParamValue} in url, sync signal with url`, + `${routePrivateProperties} became matching with params:`, + newParams, ); } - paramSignal.value = urlParamValue; + shouldLoad(route); continue; } - } - }); - // Reset flag after URL -> Signal synchronization is complete - isUpdatingRoutesFromUrl = false; - - // must be after paramsSignal.value update to ensure the proxy target is set - // (so after the batch call) - const toLoadSet = new Set(); - const toReloadSet = new Set(); - const abortSignalMap = new Map(); - const routeLoadRequestedMap = new Map(); - - const shouldLoadOrReload = (route, shouldLoad) => { - const routeAction = route.action; - const currentAction = routeAction.getCurrentAction(); - if (shouldLoad) { - if ( - navigationType === "replace" || - currentAction.aborted || - currentAction.error - ) { - shouldLoad = false; - } - } - if (shouldLoad) { - toLoadSet.add(currentAction); - } else { - toReloadSet.add(currentAction); - } - routeLoadRequestedMap.set(route, currentAction); - // Create a new abort controller for this action - const actionAbortController = new AbortController(); - actionAbortControllerWeakMap.set(currentAction, actionAbortController); - abortSignalMap.set(currentAction, actionAbortController.signal); - }; - - const shouldLoad = (route) => { - shouldLoadOrReload(route, true); - }; - const shouldReload = (route) => { - shouldLoadOrReload(route, false); - }; - const shouldAbort = (route) => { - const routeAction = route.action; - const currentAction = routeAction.getCurrentAction(); - const actionAbortController = - actionAbortControllerWeakMap.get(currentAction); - if (actionAbortController) { - actionAbortController.abort(`route no longer matching`); - actionAbortControllerWeakMap.delete(currentAction); - } - }; - - for (const { - route, - routePrivateProperties, - newMatching, - oldMatching, - newParams, - oldParams, - } of routeMatchInfoSet) { - const routeAction = route.action; - if (!routeAction) { - continue; - } - - const becomesMatching = newMatching && !oldMatching; - const becomesNotMatching = !newMatching && oldMatching; - const paramsChangedWhileMatching = - newMatching && oldMatching && newParams !== oldParams; - - // Handle actions for routes that become matching - if (becomesMatching) { - if (DEBUG) { - console.debug( - `${routePrivateProperties} became matching with params:`, - newParams, - ); + // Handle actions for routes that become not matching - abort them + if (becomesNotMatching && ROUTE_DEACTIVATION_STRATEGY === "abort") { + shouldAbort(route); + continue; } - shouldLoad(route); - continue; - } - - // Handle actions for routes that become not matching - abort them - if (becomesNotMatching && ROUTE_DEACTIVATION_STRATEGY === "abort") { - shouldAbort(route); - continue; - } - // Handle parameter changes while route stays matching - if (paramsChangedWhileMatching) { - if (DEBUG) { - console.debug(`${routePrivateProperties} params changed:`, newParams); + // Handle parameter changes while route stays matching + if (paramsChangedWhileMatching) { + if (DEBUG) { + console.debug(`${routePrivateProperties} params changed:`, newParams); + } + shouldReload(route); } - shouldReload(route); } + Object.assign(returnValue, { + loadSet: toLoadSet, + reloadSet: toReloadSet, + abortSignalMap, + routeLoadRequestedMap, + }); } - return { - loadSet: toLoadSet, - reloadSet: toReloadSet, - abortSignalMap, - routeLoadRequestedMap, - matchingRouteSet, - }; + return returnValue; }; const registerRoute = (routePattern) => { @@ -500,10 +519,8 @@ const registerRoute = (routePattern) => { matching: false, params: ROUTE_NOT_MATCHING_PARAMS, buildUrl: null, - bindAction: null, relativeUrl: null, url: null, - action: null, matchingSignal: null, paramsSignal: null, urlSignal: null, @@ -512,6 +529,10 @@ const registerRoute = (routePattern) => { toString: () => { return `route "${cleanPattern}"`; }, + + bindAction: null, + action: null, + actionStatusSignal: null, }; routeSet.add(route); const routePrivateProperties = { @@ -800,7 +821,14 @@ const registerRoute = (routePattern) => { cleanupCallbackSet.add(disposeRelativeUrlEffect); cleanupCallbackSet.add(disposeUrlEffect); - // action stuff (for later) + // action + route.actionStatusSignal = signal({ + loading: false, + error: null, + aborted: false, + completed: true, + data: undefined, + }); route.bindAction = (action) => { const { store } = action.meta; if (store) { @@ -830,6 +858,10 @@ const registerRoute = (routePattern) => { const actionBoundToThisRoute = action.bindParams(route.paramsSignal); route.action = actionBoundToThisRoute; + route.actionStatusSignal = computed(() => { + const actionStatus = getActionStatus(actionBoundToThisRoute); + return actionStatus; + }); return actionBoundToThisRoute; }; From 393f83ccba8caa8fc04cfaab3676212cbeee87a9 Mon Sep 17 00:00:00 2001 From: dmail Date: Mon, 2 Mar 2026 17:48:28 +0100 Subject: [PATCH 04/25] work --- .../navi/src/layout/details/details.jsx | 36 +++++++++---------- .../src/client/explorer/explorer.jsx | 9 +++-- .../src/client/explorer/explorer_group.jsx | 23 +++++------- 3 files changed, 30 insertions(+), 38 deletions(-) diff --git a/packages/frontend/navi/src/layout/details/details.jsx b/packages/frontend/navi/src/layout/details/details.jsx index 43ff11bbe8..d444ca9ab5 100644 --- a/packages/frontend/navi/src/layout/details/details.jsx +++ b/packages/frontend/navi/src/layout/details/details.jsx @@ -1,6 +1,6 @@ import { elementIsFocusable, findAfter } from "@jsenv/dom"; import { forwardRef } from "preact/compat"; -import { useEffect, useImperativeHandle, useRef, useState } from "preact/hooks"; +import { useEffect, useRef, useState } from "preact/hooks"; import { ActionRenderer } from "../../action/action_renderer.jsx"; import { renderActionableComponent } from "../../action/render_actionable_component.jsx"; @@ -57,7 +57,7 @@ export const Details = forwardRef((props, ref) => { }); }); -const DetailsBasic = forwardRef((props, ref) => { +const DetailsBasic = (props) => { const { id, label = "Summary", @@ -73,12 +73,12 @@ const DetailsBasic = forwardRef((props, ref) => { children, ...rest } = props; - const innerRef = useRef(); - useImperativeHandle(ref, () => innerRef.current); + const defaultRef = useRef(); + const ref = rest.ref || defaultRef; const [navState, setNavState] = useNavState(id); const [innerOpen, innerOpenSetter] = useState(open || navState); - useFocusGroup(innerRef, { + useFocusGroup(ref, { enabled: focusGroup, name: typeof focusGroup === "string" ? focusGroup : undefined, direction: focusGroupDirection, @@ -100,7 +100,7 @@ const DetailsBasic = forwardRef((props, ref) => { */ const summaryRef = useRef(null); - useKeyboardShortcuts(innerRef, [ + useKeyboardShortcuts(ref, [ { key: openKeyShortcut, enabled: arrowKeyShortcuts, @@ -109,7 +109,7 @@ const DetailsBasic = forwardRef((props, ref) => { // avoid handling openKeyShortcut twice when keydown occurs inside nested details !e.defaultPrevented, action: (e) => { - const details = innerRef.current; + const details = ref.current; if (!details.open) { e.preventDefault(); details.open = true; @@ -132,11 +132,11 @@ const DetailsBasic = forwardRef((props, ref) => { key: closeKeyShortcut, enabled: arrowKeyShortcuts, when: () => { - const details = innerRef.current; + const details = ref.current; return details.open; }, action: (e) => { - const details = innerRef.current; + const details = ref.current; const summary = summaryRef.current; if (document.activeElement === summary) { e.preventDefault(); @@ -158,7 +158,7 @@ const DetailsBasic = forwardRef((props, ref) => { return (
{ {children}
); -}); +}; -const DetailsWithAction = forwardRef((props, ref) => { +const DetailsWithAction = (props) => { const { action, loading, @@ -202,16 +202,16 @@ const DetailsWithAction = forwardRef((props, ref) => { children, ...rest } = props; - const innerRef = useRef(); - useImperativeHandle(ref, () => innerRef.current); + const defaultRef = useRef(); + const ref = rest.ref || defaultRef; const effectiveAction = useAction(action); const { loading: actionLoading } = useActionStatus(effectiveAction); - const executeAction = useExecuteAction(innerRef, { + const executeAction = useExecuteAction(ref, { // the error will be displayed by actionRenderer inside
errorEffect: "none", }); - useActionEvents(innerRef, { + useActionEvents(ref, { onPrevented: onActionPrevented, onAction: (e) => { executeAction(e); @@ -224,7 +224,7 @@ const DetailsWithAction = forwardRef((props, ref) => { return ( { const isOpen = toggleEvent.newState === "open"; @@ -242,4 +242,4 @@ const DetailsWithAction = forwardRef((props, ref) => { {children} ); -}); +}; diff --git a/packages/related/plugin-database-manager/src/client/explorer/explorer.jsx b/packages/related/plugin-database-manager/src/client/explorer/explorer.jsx index 9d38edd013..8676ec1965 100644 --- a/packages/related/plugin-database-manager/src/client/explorer/explorer.jsx +++ b/packages/related/plugin-database-manager/src/client/explorer/explorer.jsx @@ -1,6 +1,7 @@ import { initFlexDetailsSet } from "@jsenv/dom"; -import { useRunOnMount } from "@jsenv/navi"; +import { Icon, useRunOnMount } from "@jsenv/navi"; import { useLayoutEffect, useRef, useState } from "preact/hooks"; + // import { DatabaseSvg } from "../database/database_icons.jsx"; // import { useCurrentDatabase } from "../database/database_signals.js"; import { @@ -29,8 +30,6 @@ import "./explorer.css" with { type: "css" }; import "./explorer_store.js"; import { EXPLORER } from "./explorer_store.js"; -const FontSizedSvg = (props) => props; - export const Explorer = () => { useRunOnMount(EXPLORER.GET, Explorer); @@ -41,9 +40,9 @@ export const Explorer = () => { return (
+ ); }; From a96046327020eec5637d27e4cbf84d5a4b902a28 Mon Sep 17 00:00:00 2001 From: dmail Date: Mon, 2 Mar 2026 17:56:58 +0100 Subject: [PATCH 07/25] update demo --- .../layout/details/demos/details_demo.html | 739 ++++++++++-------- 1 file changed, 426 insertions(+), 313 deletions(-) diff --git a/packages/frontend/navi/src/layout/details/demos/details_demo.html b/packages/frontend/navi/src/layout/details/demos/details_demo.html index ed13c4eb1f..819487dd94 100644 --- a/packages/frontend/navi/src/layout/details/demos/details_demo.html +++ b/packages/frontend/navi/src/layout/details/demos/details_demo.html @@ -1,53 +1,99 @@ - + - - Demo - Details Component + + + Details Component Demo -
-

Details Component Demo

- -

- The Details component is an enhanced version of the HTML - <details> element with action integration, keyboard - navigation, and state management. -

- -

Features

-
    -
  • Standard details/summary collapsible behavior
  • -
  • Action integration - fetch data when opening
  • -
  • Keyboard shortcuts (Arrow keys navigation)
  • -
  • Browser navigation state integration
  • -
  • Focus group support
  • -
  • Loading states with animated marker
  • -
  • Customizable labels and styling
  • -
- -

Basic Usage

-
-

Simple Details

-

Basic collapsible content without any special behavior:

- -
-
- Simple Details Example -
-

- This is the content inside the details element. It can be - collapsed and expanded by clicking the summary. -

-

- You can put any content here - text, images, forms, other - components, etc. -

-
-
-
- -
- <Details label="Simple Details Example"> <p>Content goes - here</p> </Details> -
-
- -

Keyboard Navigation

-
-

Arrow Key Support

-

The Details component supports keyboard navigation:

- -
- Keyboard Shortcuts:
- • Right Arrow (→): Open details and focus first - element inside
- • Left Arrow (←): Close details and return focus to - summary
- • Tab: Navigate through focusable elements -
- -
-
- Try Keyboard Navigation -
-

Click on the summary above, then try these keys:

- - - - -
-
-
-
- -

Multiple Details

-
-

Nested Content

-

Multiple details can be used together:

- -
-
- Section 1: Getting Started -
-

- This section contains information about getting started with the - project. -

-
    -
  • Installation instructions
  • -
  • Basic configuration
  • -
  • First steps
  • -
+

Details Component Demo

+
+ + From 754c33aaf995becc654d3b9a1d2cbf83d02d3068 Mon Sep 17 00:00:00 2001 From: dmail Date: Mon, 2 Mar 2026 17:57:18 +0100 Subject: [PATCH 08/25] work --- .../layout/details/demos/details_demo.html | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/frontend/navi/src/layout/details/demos/details_demo.html b/packages/frontend/navi/src/layout/details/demos/details_demo.html index 819487dd94..3baeb6e96d 100644 --- a/packages/frontend/navi/src/layout/details/demos/details_demo.html +++ b/packages/frontend/navi/src/layout/details/demos/details_demo.html @@ -168,7 +168,6 @@ details { border: 1px solid black; } - @@ -241,12 +240,12 @@

Basic Usage

With Custom Styling -
+
-

This details element receives custom styling through className.

+

+ This details element receives custom styling through + className. +

@@ -260,17 +259,19 @@

Basic Usage

Keyboard Navigation

- Keyboard Shortcuts:
- • Right Arrow (→): Open details and focus first - element inside
- • Left Arrow (←): Close details and return focus to - summary
- • Tab: Navigate through focusable elements + Keyboard Shortcuts: +
Right Arrow (→): Open details and focus + first element inside +
Left Arrow (←): Close details and return + focus to summary +
Tab: Navigate through focusable elements
Try Keyboard Navigation -

Click on the summary below, then try the keyboard shortcuts:

+

+ Click on the summary below, then try the keyboard shortcuts: +

State Management
  • Share state across page refreshes
  • - This is useful for deep-linkable expandable sections. + + This is useful for deep-linkable expandable sections. +

    @@ -501,7 +504,7 @@

    Interactive Configuration

    Props Reference

    -{`interface DetailsProps { + {`interface DetailsProps { id?: string; // For nav state integration label?: string; // Summary text (default: "Summary") open?: boolean; // Initial open state From 7013409c19f7c9556575baa959661c8313b41281 Mon Sep 17 00:00:00 2001 From: dmail Date: Mon, 2 Mar 2026 18:01:33 +0100 Subject: [PATCH 09/25] work --- .../src/layout/details/demos/details_demo.html | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/frontend/navi/src/layout/details/demos/details_demo.html b/packages/frontend/navi/src/layout/details/demos/details_demo.html index 3baeb6e96d..7e8d4e4425 100644 --- a/packages/frontend/navi/src/layout/details/demos/details_demo.html +++ b/packages/frontend/navi/src/layout/details/demos/details_demo.html @@ -71,13 +71,6 @@ color: #856404; } - .content-example { - margin: 10px 0; - padding: 15px; - background: #e3f2fd; - border-left: 4px solid #2196f3; - } - .code-block { margin: 15px 0; padding: 15px; @@ -159,7 +152,6 @@ } .content-example { - margin: 10px 0; padding: 15px; background: #e3f2fd; border-left: 4px solid #2196f3; @@ -168,6 +160,10 @@ details { border: 1px solid black; } + + summary { + background: lightblue; + } @@ -183,7 +179,7 @@

    Details Component Demo

    const App = () => { return (
    - + {/* */} @@ -241,7 +237,7 @@

    Basic Usage

    With Custom Styling
    - +

    This details element receives custom styling through className. From 6da1c9671e3ac8514bee04de32ffcd1b6b41ca95 Mon Sep 17 00:00:00 2001 From: dmail Date: Mon, 2 Mar 2026 18:03:38 +0100 Subject: [PATCH 10/25] work --- .../navi/src/layout/details/details.jsx | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/packages/frontend/navi/src/layout/details/details.jsx b/packages/frontend/navi/src/layout/details/details.jsx index 135edc5e9a..6c4a9b3de1 100644 --- a/packages/frontend/navi/src/layout/details/details.jsx +++ b/packages/frontend/navi/src/layout/details/details.jsx @@ -21,32 +21,34 @@ import.meta.css = /* css */ ` display: flex; flex-shrink: 0; flex-direction: column; - } - .navi_details > summary { - display: flex; - flex-shrink: 0; - flex-direction: column; - cursor: pointer; - user-select: none; - } - .summary_body { - display: flex; - width: 100%; - flex-direction: row; - align-items: center; - gap: 0.2em; - } - .summary_label { - display: flex; - padding-right: 10px; - flex: 1; - align-items: center; - gap: 0.2em; - } + summary { + display: flex; + flex-shrink: 0; + flex-direction: column; + cursor: pointer; + user-select: none; - .navi_details > summary:focus { - z-index: 1; + &:focus { + z-index: 1; + } + + .navi_summary_body { + display: flex; + width: 100%; + flex-direction: row; + align-items: center; + gap: 0.2em; + + .navi_summary_label { + display: flex; + padding-right: 10px; + flex: 1; + align-items: center; + gap: 0.2em; + } + } + } } `; @@ -178,9 +180,9 @@ const DetailsBasic = (props) => { open={innerOpen} >

    -
    +
    -
    {label}
    +
    {label}
    {children} From a869339fd391271dbf6e08e06c913c2ceba75fbc Mon Sep 17 00:00:00 2001 From: dmail Date: Mon, 2 Mar 2026 18:12:36 +0100 Subject: [PATCH 11/25] workon details --- packages/frontend/navi/src/action/actions.js | 17 +++++++++++++++-- .../navi/src/keyboard/keyboard_shortcuts.js | 3 ++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/frontend/navi/src/action/actions.js b/packages/frontend/navi/src/action/actions.js index 7630e7cd5e..0a1dab4251 100644 --- a/packages/frontend/navi/src/action/actions.js +++ b/packages/frontend/navi/src/action/actions.js @@ -676,7 +676,7 @@ export const createAction = (callback, rootOptions = {}) => { } // ✅ CAS 2: Objet -> vérifier s'il contient des signals - if (newParamsOrSignal && typeof newParamsOrSignal === "object") { + if (isPlainObject(newParamsOrSignal)) { const staticParams = {}; const signalMap = new Map(); @@ -727,7 +727,7 @@ export const createAction = (callback, rootOptions = {}) => { return createActionProxyFromSignal(action, paramsSignal, options); } - // ✅ CAS 3: Primitive -> action enfant + // ✅ CAS 3: Primitive or objects like DOMEvents etc -> action enfant return createChildAction({ params: newParamsOrSignal, ...options, @@ -1395,6 +1395,19 @@ const generateActionName = (name, params) => { return `${name}${argsString}`; }; +const isPlainObject = (obj) => { + if (typeof obj !== "object" || obj === null) { + return false; + } + let proto = obj; + while (Object.getPrototypeOf(proto) !== null) { + proto = Object.getPrototypeOf(proto); + } + return ( + Object.getPrototypeOf(obj) === proto || Object.getPrototypeOf(obj) === null + ); +}; + if (import.meta.hot) { import.meta.hot.dispose(() => { abortRunningActions(); diff --git a/packages/frontend/navi/src/keyboard/keyboard_shortcuts.js b/packages/frontend/navi/src/keyboard/keyboard_shortcuts.js index ce4a537fd6..920231b78d 100644 --- a/packages/frontend/navi/src/keyboard/keyboard_shortcuts.js +++ b/packages/frontend/navi/src/keyboard/keyboard_shortcuts.js @@ -164,7 +164,8 @@ export const useKeyboardShortcuts = ( return false; } const { action } = shortcutCandidate; - return requestAction(element, action, { + const actionWithEvent = action.bindParams(keyboardEvent); + return requestAction(element, actionWithEvent, { actionOrigin: "keyboard_shortcut", event: keyboardEvent, requester: document.activeElement, From d132d7cb9739be1a5da8aad80cb426a79fb4cb34 Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 3 Mar 2026 09:35:23 +0100 Subject: [PATCH 12/25] work --- .../layout/details/demos/details_demo.html | 230 ++++++++++++------ 1 file changed, 160 insertions(+), 70 deletions(-) diff --git a/packages/frontend/navi/src/layout/details/demos/details_demo.html b/packages/frontend/navi/src/layout/details/demos/details_demo.html index 7e8d4e4425..efeac21fb1 100644 --- a/packages/frontend/navi/src/layout/details/demos/details_demo.html +++ b/packages/frontend/navi/src/layout/details/demos/details_demo.html @@ -157,6 +157,39 @@ border-left: 4px solid #2196f3; } + .comparison-container { + display: grid; + margin: 15px 0; + grid-template-columns: 1fr 1fr; + gap: 20px; + } + + .comparison-item { + padding: 15px; + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 8px; + } + + .comparison-item h4 { + margin: 0 0 15px 0; + padding: 5px; + color: #495057; + font-size: 0.9rem; + text-align: center; + background: #e9ecef; + border-radius: 4px; + } + + .user-agent-details { + margin: 15px 0; + } + + .user-agent-details summary { + font-weight: 500; + cursor: pointer; + } + details { border: 1px solid black; } @@ -179,13 +212,13 @@

    Details Component Demo

    const App = () => { return (
    - {/* */} + - - + - + +
    ); @@ -216,31 +249,61 @@

    Features

    const BasicUsageDemo = () => { return (
    -

    Basic Usage

    +

    Basic Usage & Keyboard Navigation

    - Simple Details -

    Basic collapsible content without any special behavior:

    -
    -
    -

    - This is the content inside the details element. It can be - collapsed and expanded by clicking the summary. -

    -

    - You can put any content here - text, images, forms, other - components, etc. -

    + Side by Side Comparison +

    + Compare the enhanced Details component with the standard HTML + details element. + Try keyboard navigation: Click on a summary + and use + to open/focus content, to close. +

    + +
    +
    +

    @jsenv/navi Details

    +
    +
    +

    + Enhanced with keyboard navigation and action support. +

    + + + + +
    +
    -
    + +
    +

    User Agent Details

    +
    + Standard HTML Details +
    +

    Standard HTML details element without enhancements.

    + + +
    +
    +
    +
    + +
    + Keyboard Shortcuts (Enhanced version only): +
    Opens details and focuses first element +
    Closes details and returns to summary +
    Tab Navigates through focusable elements +
    With Custom Styling
    - +

    - This details element receives custom styling through - className. + This details element can receive custom styling and + integrates with the Box component.

    @@ -250,50 +313,16 @@

    Basic Usage

    ); }; - const KeyboardNavigationDemo = () => { - return ( -
    -

    Keyboard Navigation

    -
    - Keyboard Shortcuts: -
    Right Arrow (→): Open details and focus - first element inside -
    Left Arrow (←): Close details and return - focus to summary -
    Tab: Navigate through focusable elements -
    - -
    - Try Keyboard Navigation -

    - Click on the summary below, then try the keyboard shortcuts: -

    -
    - -

    Use Tab to navigate through these focusable elements:

    - - - - - - -
    -
    -
    -
    - ); - }; - const MultipleDetailsDemo = () => { return (

    Multiple Details

    - Nested Content -

    Multiple details can be used together:

    + Sequential Sections +

    + Multiple details can be used together to create expandable + sections: +

    @@ -312,16 +341,11 @@

    Multiple Details

    Advanced functionality and customization options.

    -
    -
    -

    Details about integrating with external APIs.

    -
    -
    -
    -
    -

    How to customize the appearance and behavior.

    -
    -
    +
      +
    • Plugin architecture
    • +
    • Action integration
    • +
    • State management
    • +
    @@ -340,6 +364,72 @@

    Multiple Details

    ); }; + const NestedDetailsDemo = () => { + return ( +
    +

    Nested Details

    +
    + Hierarchical Content +

    + Details can be nested to create hierarchical expandable content: +

    + +
    +
    +

    Main documentation section with nested subsections.

    + +
    +
    +

    Complete API documentation.

    + +
    +
    +

    Component API details:

    +
      +
    • Props interface
    • +
    • Event handlers
    • +
    • CSS classes
    • +
    +
    +
    + +
    +
    +

    Action system documentation:

    +
      +
    • Creating actions
    • +
    • Binding parameters
    • +
    • Error handling
    • +
    +
    +
    +
    +
    + +
    +
    +

    Code examples and tutorials.

    + +
    +
    +

    Simple examples to get started.

    +
    +
    + +
    +
    +

    Complex integration patterns.

    +
    +
    +
    +
    +
    +
    +
    +
    + ); + }; + const ActionIntegrationDemo = () => { const getUserProfile = createAction(async ({ userId }) => { // Simulate network delay From 762550d8d9407ac5a946cf07ad5b053a8570da6a Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 3 Mar 2026 09:43:49 +0100 Subject: [PATCH 13/25] work --- .../layout/details/demos/details_demo.html | 2 +- .../src/layout/details/summary_marker.jsx | 134 +++++++++--------- 2 files changed, 71 insertions(+), 65 deletions(-) diff --git a/packages/frontend/navi/src/layout/details/demos/details_demo.html b/packages/frontend/navi/src/layout/details/demos/details_demo.html index efeac21fb1..811d3ef540 100644 --- a/packages/frontend/navi/src/layout/details/demos/details_demo.html +++ b/packages/frontend/navi/src/layout/details/demos/details_demo.html @@ -212,7 +212,7 @@

    Details Component Demo

    const App = () => { return (
    - + {/* */} diff --git a/packages/frontend/navi/src/layout/details/summary_marker.jsx b/packages/frontend/navi/src/layout/details/summary_marker.jsx index 95fb7e4a81..6dfbb88846 100644 --- a/packages/frontend/navi/src/layout/details/summary_marker.jsx +++ b/packages/frontend/navi/src/layout/details/summary_marker.jsx @@ -6,18 +6,67 @@ const rightArrowPath = "M680-480L360-160l-80-80 240-240-240-240 80-80 320 320z"; const downArrowPath = "M480-280L160-600l80-80 240 240 240-240 80 80-320 320z"; import.meta.css = /* css */ ` - .summary_marker { + .navi_summary_marker { width: 1em; height: 1em; line-height: 1em; + + .navi_summary_marker_loading_container { + transform: scale(0.3); + transition: transform 0.3s linear; + + .navi_summary_marker_background_circle, + .navi_summary_marker_foreground_circle { + opacity: 0; + transition: opacity 0.3s ease-in-out; + } + + .navi_summary_marker_foreground_circle { + stroke-dasharray: 503 1507; /* ~25% of circle perimeter */ + stroke-dashoffset: 0; + animation: progress-around-circle 1.5s linear infinite; + } + } + + .navi_summary_marker_arrow { + opacity: 1; + transition: opacity 0.3s ease-in-out; + animation-duration: 0.3s; + animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); + animation-fill-mode: forwards; + + &[data-animation-target="down"] { + animation-name: morph-to-down; + } + + &[data-animation-target="right"] { + animation-name: morph-to-right; + } + } + + &[data-loading] { + .navi_summary_marker_loading_container { + transform: scale(1); + + .navi_summary_marker_background_circle { + opacity: 0.2; + } + .navi_summary_marker_foreground_circle { + opacity: 1; + } + } + .navi_summary_marker_arrow { + opacity: 0; + } + } } - .summary_marker_svg .arrow { - animation-duration: 0.3s; - animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); - animation-fill-mode: forwards; - } - .summary_marker_svg .arrow[data-animation-target="down"] { - animation-name: morph-to-down; + @keyframes progress-around-circle { + 0% { + stroke-dashoffset: 0; + } + 100% { + stroke-dashoffset: -2010; + } } @keyframes morph-to-down { from { @@ -27,9 +76,6 @@ import.meta.css = /* css */ ` d: path("${downArrowPath}"); } } - .summary_marker_svg .arrow[data-animation-target="right"] { - animation-name: morph-to-right; - } @keyframes morph-to-right { from { d: path("${downArrowPath}"); @@ -38,47 +84,6 @@ import.meta.css = /* css */ ` d: path("${rightArrowPath}"); } } - - .summary_marker_svg .foreground_circle { - stroke-dasharray: 503 1507; /* ~25% of circle perimeter */ - stroke-dashoffset: 0; - animation: progress-around-circle 1.5s linear infinite; - } - @keyframes progress-around-circle { - 0% { - stroke-dashoffset: 0; - } - 100% { - stroke-dashoffset: -2010; - } - } - - /* fading and scaling */ - .summary_marker_svg .arrow { - opacity: 1; - transition: opacity 0.3s ease-in-out; - } - .summary_marker_svg .loading_container { - transform: scale(0.3); - transition: transform 0.3s linear; - } - .summary_marker_svg .background_circle, - .summary_marker_svg .foreground_circle { - opacity: 0; - transition: opacity 0.3s ease-in-out; - } - .summary_marker_svg[data-loading] .arrow { - opacity: 0; - } - .summary_marker_svg[data-loading] .loading_container { - transform: scale(1); - } - .summary_marker_svg[data-loading] .background_circle { - opacity: 0.2; - } - .summary_marker_svg[data-loading] .foreground_circle { - opacity: 1; - } `; export const SummaryMarker = ({ open, loading }) => { @@ -96,16 +101,17 @@ export const SummaryMarker = ({ open, loading }) => { prevOpenRef.current = open; return ( - - - + + + { opacity="0.2" /> { strokeDasharray="503 1507" /> - + Date: Tue, 3 Mar 2026 09:48:43 +0100 Subject: [PATCH 14/25] work --- .../layout/details/demos/details_demo.html | 91 +++++++------------ 1 file changed, 31 insertions(+), 60 deletions(-) diff --git a/packages/frontend/navi/src/layout/details/demos/details_demo.html b/packages/frontend/navi/src/layout/details/demos/details_demo.html index 811d3ef540..9e7993a701 100644 --- a/packages/frontend/navi/src/layout/details/demos/details_demo.html +++ b/packages/frontend/navi/src/layout/details/demos/details_demo.html @@ -212,36 +212,13 @@

    Details Component Demo

    const App = () => { return (
    - {/* */} + - -
    - ); - }; - - const FeaturesOverview = () => { - return ( -
    -

    Features

    -

    - The Details component is an enhanced version of the HTML{" "} - <details> element with action integration, - keyboard navigation, and state management. -

    -
      -
    • Standard details/summary collapsible behavior
    • -
    • Action integration - fetch data when opening
    • -
    • Keyboard shortcuts (Arrow keys navigation)
    • -
    • Browser navigation state integration
    • -
    • Focus group support
    • -
    • Loading states with animated marker
    • -
    • Customizable labels and styling
    • -
    ); }; @@ -296,18 +273,41 @@

    User Agent Details


    Tab Navigates through focusable elements
    +
    + ); + }; + + const LoadingExampleDemo = () => { + const [loading, setLoading] = useState(false); + const toggleLoading = () => { + setLoading(!loading); + }; + + return ( +
    +

    Loading State Example

    - With Custom Styling -
    - + Details with Loading State +

    + This example shows how the Details component displays a loading + indicator when the loading prop is true: +

    + +
    +

    - This details element can receive custom styling and - integrates with the Box component. + Content inside the details element. When loading is active, + the summary will show a loading indicator.

    - - +
    + +
    + +
    ); @@ -585,35 +585,6 @@

    Interactive Configuration

    ); }; - const PropsReference = () => { - return ( -
    -

    Props Reference

    -
    - {`interface DetailsProps { - id?: string; // For nav state integration - label?: string; // Summary text (default: "Summary") - open?: boolean; // Initial open state - loading?: boolean; // Show loading indicator - className?: string; // Additional CSS classes - focusGroup?: boolean | string; // Focus group integration - focusGroupDirection?: string; // Focus navigation direction - arrowKeyShortcuts?: boolean; // Enable arrow key shortcuts (default: true) - openKeyShortcut?: string; // Key to open (default: "ArrowRight") - closeKeyShortcut?: string; // Key to close (default: "ArrowLeft") - onToggle?: (event) => void; // Toggle event handler - action?: Action; // Action to execute on open - onActionPrevented?: Function; // Action prevention handler - onActionStart?: Function; // Action start handler - onActionError?: Function; // Action error handler - onActionEnd?: Function; // Action end handler - children?: ReactNode; // Content to display -}`} -
    -
    - ); - }; - render(, document.querySelector("#root")); From 61fb00324f5ea66a5bb50e2002ea8f30007e70bc Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 3 Mar 2026 09:54:55 +0100 Subject: [PATCH 15/25] work --- .../layout/details/demos/details_demo.html | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/packages/frontend/navi/src/layout/details/demos/details_demo.html b/packages/frontend/navi/src/layout/details/demos/details_demo.html index 9e7993a701..a4c35bec3d 100644 --- a/packages/frontend/navi/src/layout/details/demos/details_demo.html +++ b/packages/frontend/navi/src/layout/details/demos/details_demo.html @@ -232,15 +232,12 @@

    Basic Usage & Keyboard Navigation

    Compare the enhanced Details component with the standard HTML details element. - Try keyboard navigation: Click on a summary - and use - to open/focus content, to close.

    @jsenv/navi Details

    -
    +

    Enhanced with keyboard navigation and action support. @@ -256,7 +253,7 @@

    @jsenv/navi Details

    User Agent Details

    - Standard HTML Details + Summary text

    Standard HTML details element without enhancements.

    @@ -266,12 +263,43 @@

    User Agent Details

    -
    - Keyboard Shortcuts (Enhanced version only): +
    + + Enhanced Keyboard Navigation Design + +
    +
    + Tab Behavior: +
    Tab skips details content even when opened +
    • This helps users quickly skip entire collapsible + sections, which is often the desired behavior +
    • Similar to how radio buttons with the same name work as + a group +
    +
    + Arrow Key Navigation: +
    / Navigate through details + content (like radio button groups)
    Opens details and focuses first element + from summary
    Closes details and returns to summary -
    Tab Navigates through focusable elements -
    +
    +
    + Expand/Collapse from Inside: +
    / work even when focus is inside + the details +
    • Only applies to elements without their own left/right + key behavior (like buttons) +
    • Makes it easy to collapse/expand without having to + navigate back to the summary +
    +
    + + This design provides better keyboard navigation by default, + allowing users to efficiently skip or navigate through + collapsible content as needed. + +
    ); From 6da5d36495d06eb05ef5a78eb54e39f043d3fd1c Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 3 Mar 2026 09:59:42 +0100 Subject: [PATCH 16/25] work --- .../frontend/navi/src/layout/details/demos/details_demo.html | 2 +- packages/frontend/navi/src/layout/details/summary_marker.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/navi/src/layout/details/demos/details_demo.html b/packages/frontend/navi/src/layout/details/demos/details_demo.html index a4c35bec3d..a5c460ce90 100644 --- a/packages/frontend/navi/src/layout/details/demos/details_demo.html +++ b/packages/frontend/navi/src/layout/details/demos/details_demo.html @@ -306,7 +306,7 @@

    User Agent Details

    }; const LoadingExampleDemo = () => { - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(true); const toggleLoading = () => { setLoading(!loading); diff --git a/packages/frontend/navi/src/layout/details/summary_marker.jsx b/packages/frontend/navi/src/layout/details/summary_marker.jsx index 6dfbb88846..cf928f5f5c 100644 --- a/packages/frontend/navi/src/layout/details/summary_marker.jsx +++ b/packages/frontend/navi/src/layout/details/summary_marker.jsx @@ -103,7 +103,7 @@ export const SummaryMarker = ({ open, loading }) => { return ( Date: Tue, 3 Mar 2026 10:09:31 +0100 Subject: [PATCH 17/25] move details --- .../demos/5_details_demo.html} | 0 .../src/{layout => field}/details/details.jsx | 96 +++++++++++++------ .../details/summary_marker.jsx | 0 3 files changed, 69 insertions(+), 27 deletions(-) rename packages/frontend/navi/src/{layout/details/demos/details_demo.html => field/demos/5_details_demo.html} (100%) rename packages/frontend/navi/src/{layout => field}/details/details.jsx (69%) rename packages/frontend/navi/src/{layout => field}/details/summary_marker.jsx (100%) diff --git a/packages/frontend/navi/src/layout/details/demos/details_demo.html b/packages/frontend/navi/src/field/demos/5_details_demo.html similarity index 100% rename from packages/frontend/navi/src/layout/details/demos/details_demo.html rename to packages/frontend/navi/src/field/demos/5_details_demo.html diff --git a/packages/frontend/navi/src/layout/details/details.jsx b/packages/frontend/navi/src/field/details/details.jsx similarity index 69% rename from packages/frontend/navi/src/layout/details/details.jsx rename to packages/frontend/navi/src/field/details/details.jsx index 6c4a9b3de1..7c541739fe 100644 --- a/packages/frontend/navi/src/layout/details/details.jsx +++ b/packages/frontend/navi/src/field/details/details.jsx @@ -1,17 +1,25 @@ import { elementIsFocusable, findAfter } from "@jsenv/dom"; -import { useEffect, useRef, useState } from "preact/hooks"; +import { useContext, useEffect, useRef } from "preact/hooks"; import { ActionRenderer } from "../../action/action_renderer.jsx"; import { renderActionableComponent } from "../../action/render_actionable_component.jsx"; -import { useAction } from "../../action/use_action.js"; +import { useActionBoundToOneParam } from "../../action/use_action.js"; import { useActionStatus } from "../../action/use_action_status.js"; import { useExecuteAction } from "../../action/use_execute_action.js"; import { Box } from "../../box/box.jsx"; -import { useActionEvents } from "../../field/use_action_events.js"; -import { useFocusGroup } from "../../field/use_focus_group.js"; -import { requestAction } from "../../field/validation/custom_constraint_validation.js"; import { useKeyboardShortcuts } from "../../keyboard/keyboard_shortcuts.js"; -import { useNavState } from "../../nav/browser_integration/browser_integration.js"; +import { useActionEvents } from "../use_action_events.js"; +import { useFocusGroup } from "../use_focus_group.js"; +import { + UIStateContext, + UIStateControllerContext, + useUIState, + useUIStateController, +} from "../use_ui_state_controller.js"; +import { + forwardActionRequested, + requestAction, +} from "../validation/custom_constraint_validation.js"; import { SummaryMarker } from "./summary_marker.jsx"; import.meta.css = /* css */ ` @@ -53,18 +61,35 @@ import.meta.css = /* css */ ` `; export const Details = (props) => { + const { value = "on" } = props; + const uiStateController = useUIStateController(props, "details", { + statePropName: "open", + defaultStatePropName: "defaultOpen", + fallbackState: false, + getStateFromProp: (open) => (open ? value : undefined), + getPropFromState: Boolean, + }); + const uiState = useUIState(uiStateController); + const details = renderActionableComponent(props, { Basic: DetailsBasic, WithAction: DetailsWithAction, }); - return details; + return ( + + + {details} + + + ); }; const DetailsBasic = (props) => { + const uiStateController = useContext(UIStateControllerContext); + const uiState = useContext(UIStateContext); const { id, label = "Summary", - open, loading, focusGroup, focusGroupDirection, @@ -77,9 +102,8 @@ const DetailsBasic = (props) => { } = props; const defaultRef = useRef(); const ref = rest.ref || defaultRef; + const open = Boolean(uiState); - const [navState, setNavState] = useNavState(id); - const [innerOpen, innerOpenSetter] = useState(open || navState); useFocusGroup(ref, { enabled: focusGroup, name: typeof focusGroup === "string" ? focusGroup : undefined, @@ -168,20 +192,18 @@ const DetailsBasic = (props) => { const isOpen = e.newState === "open"; if (mountedRef.current) { if (isOpen) { - innerOpenSetter(true); - setNavState(true); + uiStateController.setUIState(true, e); } else { - innerOpenSetter(false); - setNavState(undefined); + uiStateController.setUIState(false, e); } } onToggle?.(e); }} - open={innerOpen} + open={open} >
    - +
    {label}
    @@ -191,12 +213,16 @@ const DetailsBasic = (props) => { }; const DetailsWithAction = (props) => { + const uiStateController = useContext(UIStateControllerContext); + const uiState = useContext(UIStateContext); const { action, loading, onToggle, + onCancel, onActionPrevented, onActionStart, + onActionAbort, onActionError, onActionEnd, children, @@ -204,21 +230,37 @@ const DetailsWithAction = (props) => { } = props; const defaultRef = useRef(); const ref = rest.ref || defaultRef; - - const effectiveAction = useAction(action); - const { loading: actionLoading } = useActionStatus(effectiveAction); + const [actionBoundToUIState] = useActionBoundToOneParam(action, uiState); + const actionStatus = useActionStatus(actionBoundToUIState); + const { loading: actionLoading } = actionStatus; const executeAction = useExecuteAction(ref, { // the error will be displayed by actionRenderer inside
    errorEffect: "none", }); + useActionEvents(ref, { - onPrevented: onActionPrevented, - onAction: (e) => { - executeAction(e); + onCancel: (e, reason) => { + if (reason === "blur_invalid") { + return; + } + uiStateController.resetUIState(e); + onCancel?.(e, reason); }, + onPrevented: onActionPrevented, + onRequested: (e) => forwardActionRequested(e, actionBoundToUIState), + onAction: executeAction, onStart: onActionStart, - onError: onActionError, - onEnd: onActionEnd, + onAbort: (e) => { + uiStateController.resetUIState(e); + onActionAbort?.(e); + }, + onError: (e) => { + uiStateController.resetUIState(e); + onActionError?.(e); + }, + onEnd: (e) => { + onActionEnd?.(e); + }, }); return ( @@ -229,17 +271,17 @@ const DetailsWithAction = (props) => { onToggle={(toggleEvent) => { const isOpen = toggleEvent.newState === "open"; if (isOpen) { - requestAction(toggleEvent.target, effectiveAction, { + requestAction(toggleEvent.target, actionBoundToUIState, { event: toggleEvent, method: "run", }); } else { - effectiveAction.abort(); + actionBoundToUIState.abort(); } onToggle?.(toggleEvent); }} > - {children} + {children} ); }; diff --git a/packages/frontend/navi/src/layout/details/summary_marker.jsx b/packages/frontend/navi/src/field/details/summary_marker.jsx similarity index 100% rename from packages/frontend/navi/src/layout/details/summary_marker.jsx rename to packages/frontend/navi/src/field/details/summary_marker.jsx From 9b6481a187c773ab741c6c53d45123bf88f3b58f Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 3 Mar 2026 10:15:50 +0100 Subject: [PATCH 18/25] workon details --- packages/frontend/navi/index.js | 11 +++++++---- .../frontend/navi/src/field/details/details.jsx | 15 +++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/frontend/navi/index.js b/packages/frontend/navi/index.js index 972f949dca..bb6edea397 100644 --- a/packages/frontend/navi/index.js +++ b/packages/frontend/navi/index.js @@ -62,6 +62,10 @@ export { Tab, TabList } from "./src/nav/tablist/tablist.jsx"; // debug/tests export { enableDebugOnDocumentLoading } from "./src/nav/browser_integration/document_loading_signal.js"; +// Details (in between navigation/interaction and fields) +export { Details } from "./src/field/details/details.jsx"; +export { SummaryMarker } from "./src/field/details/summary_marker.jsx"; + // Form // Validation export { createAvailableConstraint } from "./src/field/validation/constraints/create_available_constraint.js"; @@ -76,7 +80,7 @@ export { removeCustomMessage, } from "./src/field/validation/custom_message.js"; export { useConstraintValidityState } from "./src/field/validation/hooks/use_constraint_validity_state.js"; -// popover (callout, dialogs, ...) +// Popover (callout, dialogs, ...) export { openCallout } from "./src/field/validation/callout/callout.js"; export { useCalloutClose } from "./src/field/validation/callout/callout.jsx"; // Selection @@ -142,8 +146,6 @@ export { Svg } from "./src/graphic/svg.jsx"; export { SVGMaskOverlay } from "./src/graphic/svg_mask_overlay.jsx"; // Layout -export { Details } from "./src/layout/details/details.jsx"; -export { SummaryMarker } from "./src/layout/details/summary_marker.jsx"; export { DialogLayout } from "./src/layout/dialog_layout.jsx"; export { Separator } from "./src/layout/separator.jsx"; export { ViewportLayout } from "./src/layout/viewport_layout.jsx"; @@ -152,9 +154,10 @@ export { ViewportLayout } from "./src/layout/viewport_layout.jsx"; export { useFocusGroup } from "./src/field/use_focus_group.js"; export { useDependenciesDiff } from "./src/utils/use_dependencies_diff.js"; -// keyboard +// Keyboard export { useKeyboardShortcuts } from "./src/keyboard/keyboard_shortcuts.js"; +// More graphic stuff export { CheckSvg } from "./src/graphic/icons/check_svg.jsx"; export { ConstructionSvg } from "./src/graphic/icons/construction_svg.jsx"; export { ExclamationSvg } from "./src/graphic/icons/exclamation_svg.jsx"; diff --git a/packages/frontend/navi/src/field/details/details.jsx b/packages/frontend/navi/src/field/details/details.jsx index 7c541739fe..034ee0dc0c 100644 --- a/packages/frontend/navi/src/field/details/details.jsx +++ b/packages/frontend/navi/src/field/details/details.jsx @@ -3,7 +3,7 @@ import { useContext, useEffect, useRef } from "preact/hooks"; import { ActionRenderer } from "../../action/action_renderer.jsx"; import { renderActionableComponent } from "../../action/render_actionable_component.jsx"; -import { useActionBoundToOneParam } from "../../action/use_action.js"; +import { useAction } from "../../action/use_action.js"; import { useActionStatus } from "../../action/use_action_status.js"; import { useExecuteAction } from "../../action/use_execute_action.js"; import { Box } from "../../box/box.jsx"; @@ -214,7 +214,6 @@ const DetailsBasic = (props) => { const DetailsWithAction = (props) => { const uiStateController = useContext(UIStateControllerContext); - const uiState = useContext(UIStateContext); const { action, loading, @@ -230,8 +229,8 @@ const DetailsWithAction = (props) => { } = props; const defaultRef = useRef(); const ref = rest.ref || defaultRef; - const [actionBoundToUIState] = useActionBoundToOneParam(action, uiState); - const actionStatus = useActionStatus(actionBoundToUIState); + const effectiveAction = useAction(action); + const actionStatus = useActionStatus(effectiveAction); const { loading: actionLoading } = actionStatus; const executeAction = useExecuteAction(ref, { // the error will be displayed by actionRenderer inside
    @@ -247,7 +246,7 @@ const DetailsWithAction = (props) => { onCancel?.(e, reason); }, onPrevented: onActionPrevented, - onRequested: (e) => forwardActionRequested(e, actionBoundToUIState), + onRequested: (e) => forwardActionRequested(e, effectiveAction), onAction: executeAction, onStart: onActionStart, onAbort: (e) => { @@ -271,17 +270,17 @@ const DetailsWithAction = (props) => { onToggle={(toggleEvent) => { const isOpen = toggleEvent.newState === "open"; if (isOpen) { - requestAction(toggleEvent.target, actionBoundToUIState, { + requestAction(toggleEvent.target, effectiveAction, { event: toggleEvent, method: "run", }); } else { - actionBoundToUIState.abort(); + effectiveAction.abort(); } onToggle?.(toggleEvent); }} > - {children} + {children} ); }; From c5c81144d26217f6d2147a421e1b323857166f56 Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 3 Mar 2026 10:18:01 +0100 Subject: [PATCH 19/25] workon details --- .../navi/src/field/demos/5_details_demo.html | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/frontend/navi/src/field/demos/5_details_demo.html b/packages/frontend/navi/src/field/demos/5_details_demo.html index a5c460ce90..6a1c8131fc 100644 --- a/packages/frontend/navi/src/field/demos/5_details_demo.html +++ b/packages/frontend/navi/src/field/demos/5_details_demo.html @@ -207,7 +207,6 @@

    Details Component Demo

    import { render } from "preact"; import { useState } from "preact/hooks"; import { Details, Box, Button } from "@jsenv/navi"; - import { createAction } from "@jsenv/navi"; const App = () => { return ( @@ -459,17 +458,6 @@

    Nested Details

    }; const ActionIntegrationDemo = () => { - const getUserProfile = createAction(async ({ userId }) => { - // Simulate network delay - await new Promise((resolve) => setTimeout(resolve, 1500)); - return { - id: userId, - name: "John Doe", - email: "john.doe@example.com", - role: "Developer", - }; - }); - return (

    Action Integration

    @@ -481,7 +469,15 @@

    Action Integration

    { + await new Promise((resolve) => setTimeout(resolve, 1_500)); // Simulate network delay + return { + id: 123, + name: "John Doe", + email: "john.doe@example.com", + role: "Developer", + }; + }} label="User Profile (loads data on open)" >
    From 43d3d0dfbbf4caab593daeba5a30691a9a663137 Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 3 Mar 2026 10:25:59 +0100 Subject: [PATCH 20/25] workon details --- packages/frontend/navi/src/field/demos/5_details_demo.html | 6 +++--- packages/frontend/navi/src/field/details/details.jsx | 6 +++--- .../src/field/validation/custom_constraint_validation.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/frontend/navi/src/field/demos/5_details_demo.html b/packages/frontend/navi/src/field/demos/5_details_demo.html index 6a1c8131fc..69d970d687 100644 --- a/packages/frontend/navi/src/field/demos/5_details_demo.html +++ b/packages/frontend/navi/src/field/demos/5_details_demo.html @@ -590,9 +590,9 @@

    Interactive Configuration

    loading={loading} open={open} arrowKeyShortcuts={arrowKeys} - onToggle={(e) => { - console.log("Details toggled:", e.newState); - setOpen(e.newState === "open"); + onUIStateChange={(open) => { + console.log("Details toggled:", open); + setOpen(open); }} >
    diff --git a/packages/frontend/navi/src/field/details/details.jsx b/packages/frontend/navi/src/field/details/details.jsx index 034ee0dc0c..d89d26375e 100644 --- a/packages/frontend/navi/src/field/details/details.jsx +++ b/packages/frontend/navi/src/field/details/details.jsx @@ -17,8 +17,8 @@ import { useUIStateController, } from "../use_ui_state_controller.js"; import { + dispatchActionRequestedCustomEvent, forwardActionRequested, - requestAction, } from "../validation/custom_constraint_validation.js"; import { SummaryMarker } from "./summary_marker.jsx"; @@ -270,9 +270,9 @@ const DetailsWithAction = (props) => { onToggle={(toggleEvent) => { const isOpen = toggleEvent.newState === "open"; if (isOpen) { - requestAction(toggleEvent.target, effectiveAction, { + dispatchActionRequestedCustomEvent(toggleEvent.target, { event: toggleEvent, - method: "run", + requester: toggleEvent.target, }); } else { effectiveAction.abort(); diff --git a/packages/frontend/navi/src/field/validation/custom_constraint_validation.js b/packages/frontend/navi/src/field/validation/custom_constraint_validation.js index f1db8a1ec5..a391c44085 100644 --- a/packages/frontend/navi/src/field/validation/custom_constraint_validation.js +++ b/packages/frontend/navi/src/field/validation/custom_constraint_validation.js @@ -904,7 +904,7 @@ const getFirstButtonSubmittingForm = (form) => { ); }; -const dispatchActionRequestedCustomEvent = ( +export const dispatchActionRequestedCustomEvent = ( elementWithAction, { actionOrigin = "action_prop", event, requester }, ) => { From f80903d14c86fa0a4c6b2d8ef4143d678f75ec49 Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 3 Mar 2026 10:54:02 +0100 Subject: [PATCH 21/25] valid react element awlays displayed by action --- .../navi/src/action/action_renderer.jsx | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/frontend/navi/src/action/action_renderer.jsx b/packages/frontend/navi/src/action/action_renderer.jsx index 0a9496b0cf..0fe47d3307 100644 --- a/packages/frontend/navi/src/action/action_renderer.jsx +++ b/packages/frontend/navi/src/action/action_renderer.jsx @@ -1,3 +1,4 @@ +import { isValidElement } from "preact"; import { useErrorBoundary, useLayoutEffect } from "preact/hooks"; import { getActionPrivateProperties } from "./action_private_properties.js"; @@ -23,6 +24,23 @@ const renderErrorDefault = (error) => { const renderCompletedDefault = () => null; export const ActionRenderer = ({ action, children, disabled }) => { + if (action === undefined) { + throw new Error( + "ActionRenderer requires an action to render, but none was provided.", + ); + } + console.log(children); + let renderBranches; + if (typeof children === "function") { + renderBranches = { completed: children }; + } else if (isValidElement(children)) { + renderBranches = { always: () => children }; + } else if (isPlainObject(children)) { + renderBranches = children; + } else { + renderBranches = {}; + } + const { idle: renderIdle = renderIdleDefault, loading: renderLoading = renderLoadingDefault, @@ -30,17 +48,7 @@ export const ActionRenderer = ({ action, children, disabled }) => { error: renderError = renderErrorDefault, completed: renderCompleted, always: renderAlways, - } = typeof children === "function" ? { completed: children } : children || {}; - - if (disabled) { - return null; - } - - if (action === undefined) { - throw new Error( - "ActionRenderer requires an action to render, but none was provided.", - ); - } + } = renderBranches; const { idle, loading, aborted, error, data } = useActionStatus(action); const UIRenderedPromise = useUIRenderedPromise(action); const [errorBoundary, resetErrorBoundary] = useErrorBoundary(); @@ -66,11 +74,13 @@ export const ActionRenderer = ({ action, children, disabled }) => { }; }, [action]); + if (disabled) { + return null; + } // If renderAlways is provided, it wins and handles all rendering if (renderAlways) { return renderAlways({ idle, loading, aborted, error, data }); } - if (idle) { return renderIdle(action); } @@ -100,7 +110,6 @@ export const ActionRenderer = ({ action, children, disabled }) => { } return renderLoading(action); } - return renderCompletedSafe(data, action); }; @@ -124,3 +133,16 @@ const useUIRenderedPromise = (action) => { actionUIRenderedPromiseWeakMap.set(action, promise); return promise; }; + +const isPlainObject = (obj) => { + if (typeof obj !== "object" || obj === null) { + return false; + } + let proto = obj; + while (Object.getPrototypeOf(proto) !== null) { + proto = Object.getPrototypeOf(proto); + } + return ( + Object.getPrototypeOf(obj) === proto || Object.getPrototypeOf(obj) === null + ); +}; From 01cc216146736bcff68e6c643dd2e48ef47cea56 Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 3 Mar 2026 10:59:38 +0100 Subject: [PATCH 22/25] work --- .../frontend/navi/src/field/demos/5_details_demo.html | 1 + packages/frontend/navi/src/field/details/details.jsx | 3 ++- .../frontend/navi/src/field/use_ui_state_controller.js | 10 +++++++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/frontend/navi/src/field/demos/5_details_demo.html b/packages/frontend/navi/src/field/demos/5_details_demo.html index 69d970d687..a6feacb938 100644 --- a/packages/frontend/navi/src/field/demos/5_details_demo.html +++ b/packages/frontend/navi/src/field/demos/5_details_demo.html @@ -511,6 +511,7 @@

    State Management

    diff --git a/packages/frontend/navi/src/field/details/details.jsx b/packages/frontend/navi/src/field/details/details.jsx index d89d26375e..f2a0506274 100644 --- a/packages/frontend/navi/src/field/details/details.jsx +++ b/packages/frontend/navi/src/field/details/details.jsx @@ -61,13 +61,14 @@ import.meta.css = /* css */ ` `; export const Details = (props) => { - const { value = "on" } = props; + const { value = "on", persists } = props; const uiStateController = useUIStateController(props, "details", { statePropName: "open", defaultStatePropName: "defaultOpen", fallbackState: false, getStateFromProp: (open) => (open ? value : undefined), getPropFromState: Boolean, + persists, }); const uiState = useUIState(uiStateController); diff --git a/packages/frontend/navi/src/field/use_ui_state_controller.js b/packages/frontend/navi/src/field/use_ui_state_controller.js index 588cc6437f..f82a4d8e9c 100644 --- a/packages/frontend/navi/src/field/use_ui_state_controller.js +++ b/packages/frontend/navi/src/field/use_ui_state_controller.js @@ -62,11 +62,15 @@ export const useUIStateController = ( getStateFromProp = (prop) => prop, getPropFromState = (state) => state, getStateFromParent, + persists, } = {}, ) => { const parentUIStateController = useContext(ParentUIStateControllerContext); const formContext = useContext(FormContext); const { id, name, onUIStateChange, action } = props; + if (persists === undefined && formContext) { + persists = true; + } const uncontrolled = !formContext && !action; const [navState, setNavState] = useNavState(id); @@ -83,7 +87,7 @@ export const useUIStateController = ( // not controlled but want an initial state (a value or being checked) return getStateFromProp(defaultState); } - if (formContext && navState) { + if (persists && navState) { // not controlled but want to use value from nav state // (I think this should likely move earlier to win over the hasUIStateProp when it's undefined) return getStateFromProp(navState); @@ -194,7 +198,7 @@ export const useUIStateController = ( getStateFromProp, setUIState: (prop, e) => { const newUIState = uiStateController.getStateFromProp(prop); - if (formContext) { + if (persists) { setNavState(prop); } const currentUIState = uiStateController.uiState; @@ -215,7 +219,7 @@ export const useUIStateController = ( }, actionEnd: () => { debugUIState(`"${componentType}" actionEnd called`); - if (formContext) { + if (persists) { setNavState(undefined); } }, From 462f137e6ac3bd189a1dde269a13113fbdbaff7b Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 3 Mar 2026 11:02:00 +0100 Subject: [PATCH 23/25] work --- .../navi/src/field/demos/5_details_demo.html | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/frontend/navi/src/field/demos/5_details_demo.html b/packages/frontend/navi/src/field/demos/5_details_demo.html index a6feacb938..4ee8e9fac8 100644 --- a/packages/frontend/navi/src/field/demos/5_details_demo.html +++ b/packages/frontend/navi/src/field/demos/5_details_demo.html @@ -504,8 +504,9 @@

    State Management

    Navigation State Integration

    - Details state can be synchronized with the browser's navigation - state: + Details state can be persisted using browser history when both{" "} + id and persists props are provided (or + when the details is within a form):

    State Management >

    - When using the id prop, the Details component - will: + When using both id and persists{" "} + props, the Details component will:

      -
    • Remember its open/closed state in the URL
    • +
    • + Remember its open/closed state in browser + history/navigation API +
    • Restore state when navigating back/forward
    • -
    • Share state across page refreshes
    • +
    • Preserve state across regular page refreshes (F5)
    • +
    • + Note: The URL itself is not updated +
    +

    + State Lifetime: The state persistence is + tied to browser history and navigation behavior. Hard + refreshes (Ctrl+F5) or other forms of navigation may clear + this state, as it depends on how the user interacts with the + page. +

    - This is useful for deep-linkable expandable sections. + This is useful for maintaining UI state across normal page + interactions while avoiding URL pollution.

    From 9ca197ff6d02e2fb29be8b4ad6e7124eb1653e40 Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 3 Mar 2026 11:08:39 +0100 Subject: [PATCH 24/25] work --- packages/frontend/navi/src/action/action_renderer.jsx | 1 - .../frontend/navi/src/keyboard/keyboard_shortcuts.js | 3 +++ .../src/client/components/icon_and_text.jsx | 10 ++++++++++ .../src/client/components/text_and_count.jsx | 7 +++++++ .../src/client/database/databases_details.jsx | 3 +-- .../plugin-database-manager/src/client/layout/page.jsx | 2 +- .../role_can_login/role_can_login_list_details.jsx | 3 +-- .../client/role/role_group/role_group_list_details.jsx | 3 +-- .../role_with_ownership_list_details.jsx | 6 +++--- .../src/client/table/tables_details.jsx | 3 +-- 10 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 packages/related/plugin-database-manager/src/client/components/icon_and_text.jsx create mode 100644 packages/related/plugin-database-manager/src/client/components/text_and_count.jsx diff --git a/packages/frontend/navi/src/action/action_renderer.jsx b/packages/frontend/navi/src/action/action_renderer.jsx index 0fe47d3307..5160ff8dae 100644 --- a/packages/frontend/navi/src/action/action_renderer.jsx +++ b/packages/frontend/navi/src/action/action_renderer.jsx @@ -29,7 +29,6 @@ export const ActionRenderer = ({ action, children, disabled }) => { "ActionRenderer requires an action to render, but none was provided.", ); } - console.log(children); let renderBranches; if (typeof children === "function") { renderBranches = { completed: children }; diff --git a/packages/frontend/navi/src/keyboard/keyboard_shortcuts.js b/packages/frontend/navi/src/keyboard/keyboard_shortcuts.js index 920231b78d..d304370216 100644 --- a/packages/frontend/navi/src/keyboard/keyboard_shortcuts.js +++ b/packages/frontend/navi/src/keyboard/keyboard_shortcuts.js @@ -152,6 +152,9 @@ export const useKeyboardShortcuts = ( useEffect(() => { const element = elementRef.current; + if (!element) { + return null; + } const shortcutsCopy = []; for (const shortcutCandidate of shortcuts) { shortcutsCopy.push({ diff --git a/packages/related/plugin-database-manager/src/client/components/icon_and_text.jsx b/packages/related/plugin-database-manager/src/client/components/icon_and_text.jsx new file mode 100644 index 0000000000..7f2167149f --- /dev/null +++ b/packages/related/plugin-database-manager/src/client/components/icon_and_text.jsx @@ -0,0 +1,10 @@ +import { Icon, Text } from "@jsenv/navi"; + +export const IconAndText = ({ icon, children }) => { + return ( + + {icon} + {children} + + ); +}; diff --git a/packages/related/plugin-database-manager/src/client/components/text_and_count.jsx b/packages/related/plugin-database-manager/src/client/components/text_and_count.jsx new file mode 100644 index 0000000000..ccd82005b8 --- /dev/null +++ b/packages/related/plugin-database-manager/src/client/components/text_and_count.jsx @@ -0,0 +1,7 @@ +import { BadgeCount, Text } from "@jsenv/navi"; + +export const TextAndCount = ({ text, count }) => ( + + {text} {count} + +); diff --git a/packages/related/plugin-database-manager/src/client/database/databases_details.jsx b/packages/related/plugin-database-manager/src/client/database/databases_details.jsx index 58092fc60e..3072a307cc 100644 --- a/packages/related/plugin-database-manager/src/client/database/databases_details.jsx +++ b/packages/related/plugin-database-manager/src/client/database/databases_details.jsx @@ -1,3 +1,4 @@ +import { TextAndCount } from "../components/text_and_count.jsx"; import { useDatabaseCount } from "../database_manager_signals.js"; import { createExplorerGroupController, @@ -15,8 +16,6 @@ import { useDatabaseArrayInStore, } from "./database_store.js"; -const TextAndCount = (props) => props; - export const databasesDetailsController = createExplorerGroupController( "databases", { diff --git a/packages/related/plugin-database-manager/src/client/layout/page.jsx b/packages/related/plugin-database-manager/src/client/layout/page.jsx index c71dc011ce..908bd58c1e 100644 --- a/packages/related/plugin-database-manager/src/client/layout/page.jsx +++ b/packages/related/plugin-database-manager/src/client/layout/page.jsx @@ -2,7 +2,7 @@ import { initPositionSticky } from "@jsenv/dom"; import { ErrorBoundaryContext } from "@jsenv/navi"; import { useErrorBoundary, useLayoutEffect, useRef } from "preact/hooks"; -const IconAndText = (props) => props; +import { IconAndText } from "../components/icon_and_text.jsx"; import.meta.css = /* css */ ` .page { diff --git a/packages/related/plugin-database-manager/src/client/role/role_can_login/role_can_login_list_details.jsx b/packages/related/plugin-database-manager/src/client/role/role_can_login/role_can_login_list_details.jsx index 1e657d04a5..1bfef0b514 100644 --- a/packages/related/plugin-database-manager/src/client/role/role_can_login/role_can_login_list_details.jsx +++ b/packages/related/plugin-database-manager/src/client/role/role_can_login/role_can_login_list_details.jsx @@ -1,3 +1,4 @@ +import { TextAndCount } from "../../components/text_and_count.jsx"; import { useRoleCanLoginCount } from "../../database_manager_signals.js"; import { createExplorerGroupController, @@ -15,8 +16,6 @@ import { roleCanLoginListDetailsOpenAtStart, } from "./role_can_login_list_details_state.js"; -const TextAndCount = (props) => props; - export const roleCanLoginListDetailsController = createExplorerGroupController( "role_can_login_list", { diff --git a/packages/related/plugin-database-manager/src/client/role/role_group/role_group_list_details.jsx b/packages/related/plugin-database-manager/src/client/role/role_group/role_group_list_details.jsx index 9bb0ee73a8..8b458623ac 100644 --- a/packages/related/plugin-database-manager/src/client/role/role_group/role_group_list_details.jsx +++ b/packages/related/plugin-database-manager/src/client/role/role_group/role_group_list_details.jsx @@ -1,3 +1,4 @@ +import { TextAndCount } from "../../components/text_and_count.jsx"; import { useRoleGroupCount } from "../../database_manager_signals.js"; import { createExplorerGroupController, @@ -15,8 +16,6 @@ import { roleGroupListDetailsOpenAtStart, } from "./role_group_list_details_state.js"; -const TextAndCount = (props) => props; - export const roleGroupListDetailsController = createExplorerGroupController( "role_group_list", { diff --git a/packages/related/plugin-database-manager/src/client/role/role_with_ownership/role_with_ownership_list_details.jsx b/packages/related/plugin-database-manager/src/client/role/role_with_ownership/role_with_ownership_list_details.jsx index a7df16d892..5f3b6d0cb0 100644 --- a/packages/related/plugin-database-manager/src/client/role/role_with_ownership/role_with_ownership_list_details.jsx +++ b/packages/related/plugin-database-manager/src/client/role/role_with_ownership/role_with_ownership_list_details.jsx @@ -1,4 +1,7 @@ import { Details } from "@jsenv/navi"; + +import { IconAndText } from "../../components/icon_and_text.jsx"; +import { TextAndCount } from "../../components/text_and_count.jsx"; import { DatabaseLink } from "../../database/database_link.jsx"; import { useRoleWithOwnershipCount } from "../../database_manager_signals.js"; import { @@ -20,9 +23,6 @@ import { roleWithOwnershipListDetailsOpenAtStart, } from "./role_with_ownership_list_details_state.js"; -const IconAndText = (props) => props; -const TextAndCount = (props) => props; - import.meta.css = /* css */ ` .explorer_details { flex: 1; diff --git a/packages/related/plugin-database-manager/src/client/table/tables_details.jsx b/packages/related/plugin-database-manager/src/client/table/tables_details.jsx index f109c66f08..b7e11aa9be 100644 --- a/packages/related/plugin-database-manager/src/client/table/tables_details.jsx +++ b/packages/related/plugin-database-manager/src/client/table/tables_details.jsx @@ -1,3 +1,4 @@ +import { TextAndCount } from "../components/text_and_count.jsx"; import { useTableCount } from "../database_manager_signals.js"; import { createExplorerGroupController, @@ -11,8 +12,6 @@ import { tableListDetailsOpenAtStart, } from "./tables_details_state.js"; -const TextAndCount = (props) => props; - export const tablesDetailsController = createExplorerGroupController("tables", { detailsOpenAtStart: tableListDetailsOpenAtStart, detailsOnToggle: tableListDetailsOnToggle, From c70709f6537663e82b310f446d1ebfe59efdf872 Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 3 Mar 2026 11:23:04 +0100 Subject: [PATCH 25/25] wor --- .../src/client/components/icon_and_text.jsx | 10 ------ .../src/client/components/text_and_count.jsx | 7 ---- .../src/client/database/databases_details.jsx | 4 +-- .../src/client/explorer/explorer_group.jsx | 18 ++++++++-- .../src/client/layout/page.jsx | 16 +++------ .../role_can_login_list_details.jsx | 6 ++-- .../role_group/role_group_list_details.jsx | 6 ++-- .../role_with_ownership_list_details.jsx | 35 +++++++++---------- .../src/client/table/tables_details.jsx | 4 +-- 9 files changed, 43 insertions(+), 63 deletions(-) delete mode 100644 packages/related/plugin-database-manager/src/client/components/icon_and_text.jsx delete mode 100644 packages/related/plugin-database-manager/src/client/components/text_and_count.jsx diff --git a/packages/related/plugin-database-manager/src/client/components/icon_and_text.jsx b/packages/related/plugin-database-manager/src/client/components/icon_and_text.jsx deleted file mode 100644 index 7f2167149f..0000000000 --- a/packages/related/plugin-database-manager/src/client/components/icon_and_text.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Icon, Text } from "@jsenv/navi"; - -export const IconAndText = ({ icon, children }) => { - return ( - - {icon} - {children} - - ); -}; diff --git a/packages/related/plugin-database-manager/src/client/components/text_and_count.jsx b/packages/related/plugin-database-manager/src/client/components/text_and_count.jsx deleted file mode 100644 index ccd82005b8..0000000000 --- a/packages/related/plugin-database-manager/src/client/components/text_and_count.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import { BadgeCount, Text } from "@jsenv/navi"; - -export const TextAndCount = ({ text, count }) => ( - - {text} {count} - -); diff --git a/packages/related/plugin-database-manager/src/client/database/databases_details.jsx b/packages/related/plugin-database-manager/src/client/database/databases_details.jsx index 3072a307cc..ded4edc554 100644 --- a/packages/related/plugin-database-manager/src/client/database/databases_details.jsx +++ b/packages/related/plugin-database-manager/src/client/database/databases_details.jsx @@ -1,4 +1,3 @@ -import { TextAndCount } from "../components/text_and_count.jsx"; import { useDatabaseCount } from "../database_manager_signals.js"; import { createExplorerGroupController, @@ -35,7 +34,8 @@ export const DatabasesDetails = (props) => { detailsAction={DATABASE.GET_MANY} idKey="oid" nameKey="datname" - labelChildren={} + label="DATABASES" + count={databaseCount} renderNewButtonChildren={() => } renderItem={(database, props) => ( { detailsAction, idKey, nameKey, - labelChildren, + label, + count, renderNewButtonChildren, renderItem, useItemArrayInStore, @@ -117,7 +124,12 @@ export const ExplorerGroup = (props) => { action={detailsAction} label={ <> - {labelChildren} + + {label} + + {count} + + {renderNewButtonChildren ? ( <> diff --git a/packages/related/plugin-database-manager/src/client/layout/page.jsx b/packages/related/plugin-database-manager/src/client/layout/page.jsx index 908bd58c1e..3b76ff9091 100644 --- a/packages/related/plugin-database-manager/src/client/layout/page.jsx +++ b/packages/related/plugin-database-manager/src/client/layout/page.jsx @@ -1,9 +1,7 @@ import { initPositionSticky } from "@jsenv/dom"; -import { ErrorBoundaryContext } from "@jsenv/navi"; +import { ErrorBoundaryContext, Icon, Text } from "@jsenv/navi"; import { useErrorBoundary, useLayoutEffect, useRef } from "preact/hooks"; -import { IconAndText } from "../components/icon_and_text.jsx"; - import.meta.css = /* css */ ` .page { display: flex; @@ -107,16 +105,10 @@ export const PageHead = ({ children, spacingBottom, ...rest }) => { const PageHeadLabel = ({ icon, label, children, actions = [] }) => { const title = (

    - + {icon} + {label} - + {children}

    ); diff --git a/packages/related/plugin-database-manager/src/client/role/role_can_login/role_can_login_list_details.jsx b/packages/related/plugin-database-manager/src/client/role/role_can_login/role_can_login_list_details.jsx index 1bfef0b514..c3a623b954 100644 --- a/packages/related/plugin-database-manager/src/client/role/role_can_login/role_can_login_list_details.jsx +++ b/packages/related/plugin-database-manager/src/client/role/role_can_login/role_can_login_list_details.jsx @@ -1,4 +1,3 @@ -import { TextAndCount } from "../../components/text_and_count.jsx"; import { useRoleCanLoginCount } from "../../database_manager_signals.js"; import { createExplorerGroupController, @@ -35,9 +34,8 @@ export const RoleCanLoginListDetails = (props) => { detailsAction={ROLE_CAN_LOGIN.GET_MANY} idKey="oid" nameKey="rolname" - labelChildren={ - - } + label="ROLE LOGINS" + count={roleCanLoginCount} renderNewButtonChildren={() => } renderItem={(role, props) => ( diff --git a/packages/related/plugin-database-manager/src/client/role/role_group/role_group_list_details.jsx b/packages/related/plugin-database-manager/src/client/role/role_group/role_group_list_details.jsx index 8b458623ac..b97e7d77df 100644 --- a/packages/related/plugin-database-manager/src/client/role/role_group/role_group_list_details.jsx +++ b/packages/related/plugin-database-manager/src/client/role/role_group/role_group_list_details.jsx @@ -1,4 +1,3 @@ -import { TextAndCount } from "../../components/text_and_count.jsx"; import { useRoleGroupCount } from "../../database_manager_signals.js"; import { createExplorerGroupController, @@ -35,9 +34,8 @@ export const RoleGroupListDetails = (props) => { detailsAction={ROLE_CANNOT_LOGIN.GET_MANY} idKey="oid" nameKey="rolname" - labelChildren={ - - } + label="ROLE GROUPS" + count={roleCannotLoginCount} renderNewButtonChildren={() => } renderItem={(role, { children, ...props }) => ( diff --git a/packages/related/plugin-database-manager/src/client/role/role_with_ownership/role_with_ownership_list_details.jsx b/packages/related/plugin-database-manager/src/client/role/role_with_ownership/role_with_ownership_list_details.jsx index 5f3b6d0cb0..27af708048 100644 --- a/packages/related/plugin-database-manager/src/client/role/role_with_ownership/role_with_ownership_list_details.jsx +++ b/packages/related/plugin-database-manager/src/client/role/role_with_ownership/role_with_ownership_list_details.jsx @@ -1,7 +1,5 @@ -import { Details } from "@jsenv/navi"; +import { BadgeCount, Details, Icon, Text } from "@jsenv/navi"; -import { IconAndText } from "../../components/icon_and_text.jsx"; -import { TextAndCount } from "../../components/text_and_count.jsx"; import { DatabaseLink } from "../../database/database_link.jsx"; import { useRoleWithOwnershipCount } from "../../database_manager_signals.js"; import { @@ -54,9 +52,8 @@ export const RoleWithOwnershipListDetails = (props) => { detailsAction={ROLE_WITH_OWNERSHIP.GET_MANY} idKey="oid" nameKey="rolname" - labelChildren={ - - } + label="OWNERSHIP" + count={roleWithOwnershipCount} renderItem={(role) => { return (
    { className="explorer_details" style={{ "--details-depth": 0 }} label={ - - {role.rolname} - - } - count={role.object_count} - /> + + {pickRoleIcon(role)} + {role.rolname} + {role.object_count} + } > { rolname: role.rolname, })} label={ - + + tables + {role.table_count} + } > {(tableArray) => { @@ -136,10 +133,10 @@ export const RoleWithOwnershipListDetails = (props) => { className="explorer_details" style={{ "--details-depth": 1 }} label={ - + + databases + {role.database_count} + } action={ROLE_DATABASES.GET_MANY.bindParams({ rolname: role.rolname, diff --git a/packages/related/plugin-database-manager/src/client/table/tables_details.jsx b/packages/related/plugin-database-manager/src/client/table/tables_details.jsx index b7e11aa9be..5b59cbeaa8 100644 --- a/packages/related/plugin-database-manager/src/client/table/tables_details.jsx +++ b/packages/related/plugin-database-manager/src/client/table/tables_details.jsx @@ -1,4 +1,3 @@ -import { TextAndCount } from "../components/text_and_count.jsx"; import { useTableCount } from "../database_manager_signals.js"; import { createExplorerGroupController, @@ -28,7 +27,8 @@ export const TablesDetails = (props) => { detailsAction={TABLE.GET_MANY} idKey="oid" nameKey="tablename" - labelChildren={} + label="TABLES" + count={tableCount} renderNewButtonChildren={() => } renderItem={(table, props) => (