@contractspec/lib.design-system 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -285,10 +285,11 @@ hidden-column recovery without widening the primitive table API.
285
285
  ### Renderers and hooks
286
286
 
287
287
  - renderer exports from `./renderers`
288
- - form-contract renderer support, including readonly, password, autocomplete, address, phone, date, time, datetime, semantic FormSpec groups, grid layout hints, and text/textarea input groups
288
+ - form-contract renderer support, including readonly, password, autocomplete, address, phone, date, time, datetime, semantic FormSpec groups, grid layout hints, mobile-safe FormSpec column helper output, and text/textarea input groups
289
289
  - translation-aware rendering through `DesignSystemTranslationProvider` and `createTranslationResolver`
290
290
  - theme-aware form controls and stack primitives that consume ThemeSpec component variant props
291
- - hooks such as `useListUrlState`
291
+ - hooks such as `useListUrlState`, including scoped list filters where locked constraints are excluded from user-editable URL state
292
+ - DataViewRenderer filter chips for scoped DataView filters, including disabled locked chips on web and native surfaces
292
293
  - navigation-related shared types
293
294
 
294
295
  ### Component composition layers
@@ -1 +1 @@
1
- import{jsx as G,jsxs as w,Fragment as d}from"react/jsx-runtime";import{Pagination as b}from"@contractspec/lib.ui-kit-web/ui/atoms/Pagination";import{VStack as j}from"@contractspec/lib.ui-kit-web/ui/stack";import{Text as _}from"@contractspec/lib.ui-kit-web/ui/text";import*as k from"react";import{resolveTranslationString as F,useDesignSystemTranslation as N}from"../../i18n/translation";import{FiltersToolbar as x}from"../molecules/FiltersToolbar";import{DataViewDetail as A}from"./DataViewDetail";import{DataViewList as T}from"./DataViewList";import{DataViewTable as C}from"./DataViewTable";export function DataViewRenderer({spec:E,items:M=[],item:Y=null,className:I,renderActions:Q,onSelect:U,onRowClick:Z,toolbar:$,loading:q,headerActions:O,emptyState:K,footer:z,search:V,onSearchChange:L,filters:J,onFilterChange:P,pagination:H,onPageChange:R}){const W=N(),u=k.useMemo(()=>{switch(E.view.kind){case"list":return G(T,{spec:E,items:M,className:I,renderActions:Q,onSelect:U,emptyState:K});case"table":return G(C,{spec:E,items:M,className:I,onRowClick:Z,toolbar:$,loading:q,emptyState:K,headerActions:O,footer:z});case"detail":return G(A,{spec:E,item:Y,className:I,emptyState:K,headerActions:O});case"grid":{const B=E.view,X={kind:"list",layout:"card",fields:B.fields,filters:B.filters,actions:B.actions,primaryField:B.primaryField,secondaryFields:B.secondaryFields},v={...E,view:X};return G(T,{spec:v,items:M,className:I,renderActions:Q,onSelect:U,emptyState:K})}default:return G(_,{className:I,children:F("Unsupported data view kind",W)})}},[E,M,Y,I,Q,U,Z,$,q,O,K,z,W]);if(!(E.view.kind==="list"||E.view.kind==="table"||E.view.kind==="grid"))return G(d,{children:u});return w(j,{gap:"lg",children:[(E.view.filters?.length||L)&&G(x,{searchValue:V,onSearchChange:L,searchPlaceholder:F("Search...",W)??"Search...",activeChips:J?Object.entries(J).map(([B,X])=>({key:B,label:`${B}: ${X}`,onRemove:()=>{if(J){const{[B]:v,...D}=J;P?.(D)}}})):[],onClearAll:J&&Object.keys(J).length>0?()=>P?.({}):void 0,right:E.view.kind==="table"?void 0:O}),u,H&&H.total>0&&G(b,{currentPage:H.page,totalPages:Math.ceil(H.total/H.pageSize),totalItems:H.total,itemsPerPage:H.pageSize,onPageChange:(B)=>R?.(B),onItemsPerPageChange:(B)=>{R?.(1)},showItemsPerPage:!1})]})}
1
+ import{jsx as J,jsxs as c,Fragment as p}from"react/jsx-runtime";import{resolveDataViewFilters as S}from"@contractspec/lib.contracts-spec/data-views";import{Pagination as d}from"@contractspec/lib.ui-kit-web/ui/atoms/Pagination";import{VStack as h}from"@contractspec/lib.ui-kit-web/ui/stack";import{Text as f}from"@contractspec/lib.ui-kit-web/ui/text";import*as L from"react";import{resolveTranslationString as V,useDesignSystemTranslation as u}from"../../i18n/translation";import{FiltersToolbar as m}from"../molecules/FiltersToolbar";import{DataViewDetail as v}from"./DataViewDetail";import{DataViewList as b}from"./DataViewList";import{DataViewTable as y}from"./DataViewTable";export function DataViewRenderer({spec:B,items:G=[],item:X=null,className:M,renderActions:$,onSelect:q,onRowClick:P,toolbar:R,loading:T,headerActions:Y,emptyState:Q,footer:j,search:w,onSearchChange:_,filters:I,onFilterChange:Z,pagination:K,onPageChange:D}){const z=u(),O=L.useMemo(()=>S({filters:B.view.filters,scope:B.view.filterScope,user:g(I)}),[I,B.view.filterScope,B.view.filters]),N=L.useMemo(()=>{if(B.view.filterScope){const E=Object.entries(O.user).map(([H,W])=>({key:H,label:`${A(B,H)}: ${F(W)}`,onRemove:()=>{const{[H]:r,...C}=O.user;Z?.(C)}})),U=O.lockedChips==="hidden"?[]:Object.entries(O.locked).map(([H,W])=>({key:`locked-${H}`,label:`${A(B,H)}: ${F(W)}`,disabled:!0}));return[...E,...U]}return I?Object.entries(I).map(([E,U])=>({key:E,label:`${E}: ${String(U)}`,onRemove:()=>{const{[E]:H,...W}=I;Z?.(W)}})):[]},[I,Z,O,B]),k=B.view.filterScope?Object.keys(O.user).length>0:Boolean(I&&Object.keys(I).length>0),x=L.useMemo(()=>{switch(B.view.kind){case"list":return J(b,{spec:B,items:G,className:M,renderActions:$,onSelect:q,emptyState:Q});case"table":return J(y,{spec:B,items:G,className:M,onRowClick:P,toolbar:R,loading:T,emptyState:Q,headerActions:Y,footer:j});case"detail":return J(v,{spec:B,item:X,className:M,emptyState:Q,headerActions:Y});case"grid":{const E=B.view,U={kind:"list",layout:"card",fields:E.fields,filters:E.filters,actions:E.actions,primaryField:E.primaryField,secondaryFields:E.secondaryFields,filterScope:E.filterScope},H={...B,view:U};return J(b,{spec:H,items:G,className:M,renderActions:$,onSelect:q,emptyState:Q})}default:return J(f,{className:M,children:V("Unsupported data view kind",z)})}},[B,G,X,M,$,q,P,R,T,Y,Q,j,z]);if(!(B.view.kind==="list"||B.view.kind==="table"||B.view.kind==="grid"))return J(p,{children:x});return c(h,{gap:"lg",children:[(B.view.filters?.length||_||N.length)&&J(m,{searchValue:w,onSearchChange:_,searchPlaceholder:V("Search...",z)??"Search...",activeChips:N,onClearAll:k?()=>Z?.({}):void 0,right:B.view.kind==="table"?void 0:Y}),x,K&&K.total>0&&J(d,{currentPage:K.page,totalPages:Math.ceil(K.total/K.pageSize),totalItems:K.total,itemsPerPage:K.pageSize,onPageChange:(E)=>D?.(E),onItemsPerPageChange:(E)=>{D?.(1)},showItemsPerPage:!1})]})}function g(B){if(!B)return;return Object.fromEntries(Object.entries(B??{}).filter((G)=>Boolean(G[1]&&typeof G[1]==="object"&&"kind"in G[1]&&typeof G[1].kind==="string")))}function A(B,G){return B.view.filters?.find((X)=>X.key===G)?.label??G}function F(B){if(!B)return"";if(B.kind==="single")return String(B.value);if(B.kind==="multi")return B.values.map(String).join(", ");if(B.kind==="range")return[B.from==null?"":String(B.from),B.to==null?"":String(B.to)].filter(Boolean).join(" - ");return`${B.mode}(${B.clauses.length})`}
@@ -1 +1 @@
1
- import{jsx as E,jsxs as D}from"react/jsx-runtime";import{Badge as k}from"@contractspec/lib.ui-kit-web/ui/badge";import{cn as I}from"@contractspec/lib.ui-kit-web/ui/utils";import*as K from"react";import{Button as W}from"../atoms/Button";import{Input as N}from"../atoms/Input";export function FiltersToolbar({className:X,children:Y,right:Z,searchPlaceholder:$,searchValue:G,onSearchChange:F,onSearchSubmit:L,debounceMs:O=250,activeChips:T=[],onClearAll:H}){const[J,U]=K.useState(G??"");K.useEffect(()=>{U(G??"")},[G]);K.useEffect(()=>{if(!F)return;const z=setTimeout(()=>F(J),O);return()=>clearTimeout(z)},[J,O,F]);return D("div",{className:I("space-y-2",X),children:[D("div",{className:"flex flex-col items-stretch gap-2 md:flex-row md:items-center md:justify-between",children:[D("div",{className:"flex flex-1 items-center gap-2",children:[F?D("div",{className:"flex flex-1 items-center gap-2",children:[E(N,{value:J,onChange:(z)=>U(z.target.value),onKeyDown:(z)=>{if(z.key==="Enter")L?.()},placeholder:$,keyboard:{kind:"search"}}),E(W,{variant:"outline",onPress:()=>L?.(),className:"shrink-0",children:"Rechercher"})]}):null,Y]}),Z]}),(T.length>0||H)&&D("div",{className:"flex flex-wrap items-center gap-2",children:[T.map((z)=>D(k,{variant:"secondary",className:"inline-flex items-center gap-2",children:[E("span",{children:z.label}),z.onRemove&&E("button",{type:"button","aria-label":"Supprimer le filtre",onClick:z.onRemove,className:"rounded-xs px-1 text-base hover:bg-muted",children:"×"})]},z.key)),H&&E(W,{size:"sm",variant:"ghost",onPress:H,children:"Effacer les filtres"})]})]})}
1
+ import{jsx as E,jsxs as D}from"react/jsx-runtime";import{Badge as I}from"@contractspec/lib.ui-kit-web/ui/badge";import{cn as W}from"@contractspec/lib.ui-kit-web/ui/utils";import*as K from"react";import{Button as X}from"../atoms/Button";import{Input as N}from"../atoms/Input";export function FiltersToolbar({className:Y,children:Z,right:$,searchPlaceholder:k,searchValue:G,onSearchChange:F,onSearchSubmit:L,debounceMs:O=250,activeChips:T=[],onClearAll:H}){const[J,U]=K.useState(G??"");K.useEffect(()=>{U(G??"")},[G]);K.useEffect(()=>{if(!F)return;const z=setTimeout(()=>F(J),O);return()=>clearTimeout(z)},[J,O,F]);return D("div",{className:W("space-y-2",Y),children:[D("div",{className:"flex flex-col items-stretch gap-2 md:flex-row md:items-center md:justify-between",children:[D("div",{className:"flex flex-1 items-center gap-2",children:[F?D("div",{className:"flex flex-1 items-center gap-2",children:[E(N,{value:J,onChange:(z)=>U(z.target.value),onKeyDown:(z)=>{if(z.key==="Enter")L?.()},placeholder:k,keyboard:{kind:"search"}}),E(X,{variant:"outline",onPress:()=>L?.(),className:"shrink-0",children:"Rechercher"})]}):null,Z]}),$]}),(T.length>0||H)&&D("div",{className:"flex flex-wrap items-center gap-2",children:[T.map((z)=>D(I,{variant:"secondary",className:W("inline-flex items-center gap-2",z.disabled&&"opacity-70"),children:[E("span",{children:z.label}),z.onRemove&&!z.disabled&&E("button",{type:"button","aria-label":"Supprimer le filtre",onClick:z.onRemove,className:"rounded-xs px-1 text-base hover:bg-muted",children:"×"})]},z.key)),H&&E(X,{size:"sm",variant:"ghost",onPress:H,children:"Effacer les filtres"})]})]})}
@@ -1 +1 @@
1
- import*as a from"react";export function useListUrlState({defaults:n,paramKeys:t={q:"q",page:"page",limit:"limit",sort:"sort",filters:"f"},replaceState:d=!0}){const w=a.useCallback(()=>{if(typeof window>"u")return n;const e=new URL(window.location.href).searchParams,c=(f,o)=>{const s=f?Number(f):NaN;return Number.isFinite(s)&&s>0?s:o},r=e.get(t.filters);let i=n.filters;if(r)try{i=JSON.parse(r)}catch{i=n.filters}return{q:e.get(t.q)??n.q,page:c(e.get(t.page),n.page),limit:c(e.get(t.limit),n.limit),sort:e.get(t.sort),filters:i}},[n,t]),[g,F]=a.useState(w),u=a.useCallback((l)=>{if(typeof window>"u")return;const e=new URL(window.location.href),c=e.searchParams,r={...g,...l},i=(o,s)=>{if(s==null||s===""||s==="null")c.delete(o);else c.set(o,s)};i(t.q,r.q||null);i(t.page,String(r.page));i(t.limit,String(r.limit));i(t.sort,r.sort??null);try{const o=JSON.stringify(r.filters??{});i(t.filters,o==="{}"?null:o)}catch{}const f=`${e.pathname}?${c.toString()}${e.hash}`;if(d)window.history.replaceState({},"",f);else window.history.pushState({},"",f);F(r)},[g,t,d]),S=a.useCallback((l,e)=>{u({filters:{...g.filters,[l]:e}})},[g.filters,u]),T=a.useCallback(()=>{u({filters:{},page:1})},[u]);a.useEffect(()=>{const l=()=>F(w());window.addEventListener("popstate",l);return()=>window.removeEventListener("popstate",l)},[w]);return{state:g,setState:u,setFilter:S,clearFilters:T}}
1
+ import{createInitialListFilters as N,sanitizeListUserFilters as _}from"@contractspec/lib.presentation-runtime-core";import*as Q from"react";export function useListUrlState({defaults:E,paramKeys:h={q:"q",page:"page",limit:"limit",sort:"sort",filters:"f"},replaceState:$=!0,filterScope:J}){const Y=Q.useMemo(()=>({...N(J),..._(E.filters,J)}),[E.filters,J]),Z=Q.useCallback(()=>{if(typeof window>"u")return E;const A=new URL(window.location.href).searchParams,V=(X,M)=>{const H=X?Number(X):NaN;return Number.isFinite(H)&&H>0?H:M},C=A.get(h.filters);let B=E.filters;if(C)try{B=_(JSON.parse(C),J)}catch{B=Y}else B=Y;return{q:A.get(h.q)??E.q,page:V(A.get(h.page),E.page),limit:V(A.get(h.limit),E.limit),sort:A.get(h.sort),filters:B}},[Y,E,J,h]),[T,b]=Q.useState(Z),W=Q.useCallback((G)=>{if(typeof window>"u")return;const A=new URL(window.location.href),V=A.searchParams,C={...T,...G,filters:_(G.filters??T.filters,J)},B=(M,H)=>{if(H==null||H===""||H==="null")V.delete(M);else V.set(M,H)};B(h.q,C.q||null);B(h.page,String(C.page));B(h.limit,String(C.limit));B(h.sort,C.sort??null);try{const M=JSON.stringify(C.filters??{});B(h.filters,M==="{}"?null:M)}catch{}const X=`${A.pathname}?${V.toString()}${A.hash}`;if($)window.history.replaceState({},"",X);else window.history.pushState({},"",X);b(C)},[J,T,h,$]),I=Q.useCallback((G,A)=>{W({filters:{...T.filters,[G]:A}})},[T.filters,W]),j=Q.useCallback(()=>{W({filters:{},page:1})},[W]);Q.useEffect(()=>{const G=()=>b(Z());window.addEventListener("popstate",G);return()=>window.removeEventListener("popstate",G)},[Z]);return{state:T,setState:W,setFilter:I,clearFilters:j}}
@@ -1,4 +1,4 @@
1
- import type { DataViewSpec } from '@contractspec/lib.contracts-spec/data-views';
1
+ import type { DataViewFilterSet, DataViewSpec } from '@contractspec/lib.contracts-spec/data-views';
2
2
  import * as React from 'react';
3
3
  export interface DataViewRendererProps {
4
4
  spec: DataViewSpec;
@@ -15,8 +15,8 @@ export interface DataViewRendererProps {
15
15
  footer?: React.ReactNode;
16
16
  search?: string;
17
17
  onSearchChange?: (value: string) => void;
18
- filters?: Record<string, unknown>;
19
- onFilterChange?: (filters: Record<string, unknown>) => void;
18
+ filters?: Record<string, unknown> | DataViewFilterSet;
19
+ onFilterChange?: (filters: Record<string, unknown> | DataViewFilterSet) => void;
20
20
  pagination?: {
21
21
  page: number;
22
22
  pageSize: number;
@@ -1 +1 @@
1
- import{jsx as G,jsxs as w,Fragment as d}from"react/jsx-runtime";import{Pagination as b}from"@contractspec/lib.ui-kit-web/ui/atoms/Pagination";import{VStack as j}from"@contractspec/lib.ui-kit-web/ui/stack";import{Text as _}from"@contractspec/lib.ui-kit-web/ui/text";import*as k from"react";import{resolveTranslationString as F,useDesignSystemTranslation as N}from"../../i18n/translation";import{FiltersToolbar as x}from"../molecules/FiltersToolbar";import{DataViewDetail as A}from"./DataViewDetail";import{DataViewList as T}from"./DataViewList";import{DataViewTable as C}from"./DataViewTable";export function DataViewRenderer({spec:E,items:M=[],item:Y=null,className:I,renderActions:Q,onSelect:U,onRowClick:Z,toolbar:$,loading:q,headerActions:O,emptyState:K,footer:z,search:V,onSearchChange:L,filters:J,onFilterChange:P,pagination:H,onPageChange:R}){const W=N(),u=k.useMemo(()=>{switch(E.view.kind){case"list":return G(T,{spec:E,items:M,className:I,renderActions:Q,onSelect:U,emptyState:K});case"table":return G(C,{spec:E,items:M,className:I,onRowClick:Z,toolbar:$,loading:q,emptyState:K,headerActions:O,footer:z});case"detail":return G(A,{spec:E,item:Y,className:I,emptyState:K,headerActions:O});case"grid":{const B=E.view,X={kind:"list",layout:"card",fields:B.fields,filters:B.filters,actions:B.actions,primaryField:B.primaryField,secondaryFields:B.secondaryFields},v={...E,view:X};return G(T,{spec:v,items:M,className:I,renderActions:Q,onSelect:U,emptyState:K})}default:return G(_,{className:I,children:F("Unsupported data view kind",W)})}},[E,M,Y,I,Q,U,Z,$,q,O,K,z,W]);if(!(E.view.kind==="list"||E.view.kind==="table"||E.view.kind==="grid"))return G(d,{children:u});return w(j,{gap:"lg",children:[(E.view.filters?.length||L)&&G(x,{searchValue:V,onSearchChange:L,searchPlaceholder:F("Search...",W)??"Search...",activeChips:J?Object.entries(J).map(([B,X])=>({key:B,label:`${B}: ${X}`,onRemove:()=>{if(J){const{[B]:v,...D}=J;P?.(D)}}})):[],onClearAll:J&&Object.keys(J).length>0?()=>P?.({}):void 0,right:E.view.kind==="table"?void 0:O}),u,H&&H.total>0&&G(b,{currentPage:H.page,totalPages:Math.ceil(H.total/H.pageSize),totalItems:H.total,itemsPerPage:H.pageSize,onPageChange:(B)=>R?.(B),onItemsPerPageChange:(B)=>{R?.(1)},showItemsPerPage:!1})]})}
1
+ import{jsx as J,jsxs as c,Fragment as p}from"react/jsx-runtime";import{resolveDataViewFilters as S}from"@contractspec/lib.contracts-spec/data-views";import{Pagination as d}from"@contractspec/lib.ui-kit-web/ui/atoms/Pagination";import{VStack as h}from"@contractspec/lib.ui-kit-web/ui/stack";import{Text as f}from"@contractspec/lib.ui-kit-web/ui/text";import*as L from"react";import{resolveTranslationString as V,useDesignSystemTranslation as u}from"../../i18n/translation";import{FiltersToolbar as m}from"../molecules/FiltersToolbar";import{DataViewDetail as v}from"./DataViewDetail";import{DataViewList as b}from"./DataViewList";import{DataViewTable as y}from"./DataViewTable";export function DataViewRenderer({spec:B,items:G=[],item:X=null,className:M,renderActions:$,onSelect:q,onRowClick:P,toolbar:R,loading:T,headerActions:Y,emptyState:Q,footer:j,search:w,onSearchChange:_,filters:I,onFilterChange:Z,pagination:K,onPageChange:D}){const z=u(),O=L.useMemo(()=>S({filters:B.view.filters,scope:B.view.filterScope,user:g(I)}),[I,B.view.filterScope,B.view.filters]),N=L.useMemo(()=>{if(B.view.filterScope){const E=Object.entries(O.user).map(([H,W])=>({key:H,label:`${A(B,H)}: ${F(W)}`,onRemove:()=>{const{[H]:r,...C}=O.user;Z?.(C)}})),U=O.lockedChips==="hidden"?[]:Object.entries(O.locked).map(([H,W])=>({key:`locked-${H}`,label:`${A(B,H)}: ${F(W)}`,disabled:!0}));return[...E,...U]}return I?Object.entries(I).map(([E,U])=>({key:E,label:`${E}: ${String(U)}`,onRemove:()=>{const{[E]:H,...W}=I;Z?.(W)}})):[]},[I,Z,O,B]),k=B.view.filterScope?Object.keys(O.user).length>0:Boolean(I&&Object.keys(I).length>0),x=L.useMemo(()=>{switch(B.view.kind){case"list":return J(b,{spec:B,items:G,className:M,renderActions:$,onSelect:q,emptyState:Q});case"table":return J(y,{spec:B,items:G,className:M,onRowClick:P,toolbar:R,loading:T,emptyState:Q,headerActions:Y,footer:j});case"detail":return J(v,{spec:B,item:X,className:M,emptyState:Q,headerActions:Y});case"grid":{const E=B.view,U={kind:"list",layout:"card",fields:E.fields,filters:E.filters,actions:E.actions,primaryField:E.primaryField,secondaryFields:E.secondaryFields,filterScope:E.filterScope},H={...B,view:U};return J(b,{spec:H,items:G,className:M,renderActions:$,onSelect:q,emptyState:Q})}default:return J(f,{className:M,children:V("Unsupported data view kind",z)})}},[B,G,X,M,$,q,P,R,T,Y,Q,j,z]);if(!(B.view.kind==="list"||B.view.kind==="table"||B.view.kind==="grid"))return J(p,{children:x});return c(h,{gap:"lg",children:[(B.view.filters?.length||_||N.length)&&J(m,{searchValue:w,onSearchChange:_,searchPlaceholder:V("Search...",z)??"Search...",activeChips:N,onClearAll:k?()=>Z?.({}):void 0,right:B.view.kind==="table"?void 0:Y}),x,K&&K.total>0&&J(d,{currentPage:K.page,totalPages:Math.ceil(K.total/K.pageSize),totalItems:K.total,itemsPerPage:K.pageSize,onPageChange:(E)=>D?.(E),onItemsPerPageChange:(E)=>{D?.(1)},showItemsPerPage:!1})]})}function g(B){if(!B)return;return Object.fromEntries(Object.entries(B??{}).filter((G)=>Boolean(G[1]&&typeof G[1]==="object"&&"kind"in G[1]&&typeof G[1].kind==="string")))}function A(B,G){return B.view.filters?.find((X)=>X.key===G)?.label??G}function F(B){if(!B)return"";if(B.kind==="single")return String(B.value);if(B.kind==="multi")return B.values.map(String).join(", ");if(B.kind==="range")return[B.from==null?"":String(B.from),B.to==null?"":String(B.to)].filter(Boolean).join(" - ");return`${B.mode}(${B.clauses.length})`}
@@ -1,4 +1,4 @@
1
- import type { DataViewSpec } from '@contractspec/lib.contracts-spec/data-views';
1
+ import type { DataViewFilterSet, DataViewSpec } from '@contractspec/lib.contracts-spec/data-views';
2
2
  import * as React from 'react';
3
3
  export interface DataViewRendererProps {
4
4
  spec: DataViewSpec;
@@ -15,8 +15,8 @@ export interface DataViewRendererProps {
15
15
  footer?: React.ReactNode;
16
16
  search?: string;
17
17
  onSearchChange?: (value: string) => void;
18
- filters?: Record<string, unknown>;
19
- onFilterChange?: (filters: Record<string, unknown>) => void;
18
+ filters?: Record<string, unknown> | DataViewFilterSet;
19
+ onFilterChange?: (filters: Record<string, unknown> | DataViewFilterSet) => void;
20
20
  pagination?: {
21
21
  page: number;
22
22
  pageSize: number;
@@ -2,6 +2,7 @@ import * as React from 'react';
2
2
  export interface ActiveFilterChip {
3
3
  key: string;
4
4
  label: React.ReactNode;
5
+ disabled?: boolean;
5
6
  onRemove?: () => void;
6
7
  }
7
8
  export interface FiltersToolbarProps {
@@ -1 +1 @@
1
- import{jsx as E,jsxs as D}from"react/jsx-runtime";import{Badge as k}from"@contractspec/lib.ui-kit-web/ui/badge";import{cn as I}from"@contractspec/lib.ui-kit-web/ui/utils";import*as K from"react";import{Button as W}from"../atoms/Button";import{Input as N}from"../atoms/Input";export function FiltersToolbar({className:X,children:Y,right:Z,searchPlaceholder:$,searchValue:G,onSearchChange:F,onSearchSubmit:L,debounceMs:O=250,activeChips:T=[],onClearAll:H}){const[J,U]=K.useState(G??"");K.useEffect(()=>{U(G??"")},[G]);K.useEffect(()=>{if(!F)return;const z=setTimeout(()=>F(J),O);return()=>clearTimeout(z)},[J,O,F]);return D("div",{className:I("space-y-2",X),children:[D("div",{className:"flex flex-col items-stretch gap-2 md:flex-row md:items-center md:justify-between",children:[D("div",{className:"flex flex-1 items-center gap-2",children:[F?D("div",{className:"flex flex-1 items-center gap-2",children:[E(N,{value:J,onChange:(z)=>U(z.target.value),onKeyDown:(z)=>{if(z.key==="Enter")L?.()},placeholder:$,keyboard:{kind:"search"}}),E(W,{variant:"outline",onPress:()=>L?.(),className:"shrink-0",children:"Rechercher"})]}):null,Y]}),Z]}),(T.length>0||H)&&D("div",{className:"flex flex-wrap items-center gap-2",children:[T.map((z)=>D(k,{variant:"secondary",className:"inline-flex items-center gap-2",children:[E("span",{children:z.label}),z.onRemove&&E("button",{type:"button","aria-label":"Supprimer le filtre",onClick:z.onRemove,className:"rounded-xs px-1 text-base hover:bg-muted",children:"\xD7"})]},z.key)),H&&E(W,{size:"sm",variant:"ghost",onPress:H,children:"Effacer les filtres"})]})]})}
1
+ import{jsx as E,jsxs as D}from"react/jsx-runtime";import{Badge as I}from"@contractspec/lib.ui-kit-web/ui/badge";import{cn as W}from"@contractspec/lib.ui-kit-web/ui/utils";import*as K from"react";import{Button as X}from"../atoms/Button";import{Input as N}from"../atoms/Input";export function FiltersToolbar({className:Y,children:Z,right:$,searchPlaceholder:k,searchValue:G,onSearchChange:F,onSearchSubmit:L,debounceMs:O=250,activeChips:T=[],onClearAll:H}){const[J,U]=K.useState(G??"");K.useEffect(()=>{U(G??"")},[G]);K.useEffect(()=>{if(!F)return;const z=setTimeout(()=>F(J),O);return()=>clearTimeout(z)},[J,O,F]);return D("div",{className:W("space-y-2",Y),children:[D("div",{className:"flex flex-col items-stretch gap-2 md:flex-row md:items-center md:justify-between",children:[D("div",{className:"flex flex-1 items-center gap-2",children:[F?D("div",{className:"flex flex-1 items-center gap-2",children:[E(N,{value:J,onChange:(z)=>U(z.target.value),onKeyDown:(z)=>{if(z.key==="Enter")L?.()},placeholder:k,keyboard:{kind:"search"}}),E(X,{variant:"outline",onPress:()=>L?.(),className:"shrink-0",children:"Rechercher"})]}):null,Z]}),$]}),(T.length>0||H)&&D("div",{className:"flex flex-wrap items-center gap-2",children:[T.map((z)=>D(I,{variant:"secondary",className:W("inline-flex items-center gap-2",z.disabled&&"opacity-70"),children:[E("span",{children:z.label}),z.onRemove&&!z.disabled&&E("button",{type:"button","aria-label":"Supprimer le filtre",onClick:z.onRemove,className:"rounded-xs px-1 text-base hover:bg-muted",children:"\xD7"})]},z.key)),H&&E(X,{size:"sm",variant:"ghost",onPress:H,children:"Effacer les filtres"})]})]})}
@@ -1,3 +1,4 @@
1
+ import type { ListFilterScope } from '@contractspec/lib.presentation-runtime-core';
1
2
  export interface ListUrlState<TFilters extends Record<string, unknown> = Record<string, unknown>> {
2
3
  q: string;
3
4
  page: number;
@@ -5,8 +6,9 @@ export interface ListUrlState<TFilters extends Record<string, unknown> = Record<
5
6
  sort?: string | null;
6
7
  filters: TFilters;
7
8
  }
8
- export declare function useListUrlState<TFilters extends Record<string, unknown> = Record<string, unknown>>({ defaults, paramKeys, replaceState, }: {
9
+ export declare function useListUrlState<TFilters extends Record<string, unknown> = Record<string, unknown>>({ defaults, paramKeys, replaceState, filterScope, }: {
9
10
  defaults: ListUrlState<TFilters>;
11
+ filterScope?: ListFilterScope<TFilters>;
10
12
  paramKeys?: {
11
13
  q: string;
12
14
  page: string;
@@ -1 +1 @@
1
- import*as a from"react";export function useListUrlState({defaults:n,paramKeys:t={q:"q",page:"page",limit:"limit",sort:"sort",filters:"f"},replaceState:d=!0}){const w=a.useCallback(()=>{if(typeof window>"u")return n;const e=new URL(window.location.href).searchParams,c=(f,o)=>{const s=f?Number(f):NaN;return Number.isFinite(s)&&s>0?s:o},r=e.get(t.filters);let i=n.filters;if(r)try{i=JSON.parse(r)}catch{i=n.filters}return{q:e.get(t.q)??n.q,page:c(e.get(t.page),n.page),limit:c(e.get(t.limit),n.limit),sort:e.get(t.sort),filters:i}},[n,t]),[g,F]=a.useState(w),u=a.useCallback((l)=>{if(typeof window>"u")return;const e=new URL(window.location.href),c=e.searchParams,r={...g,...l},i=(o,s)=>{if(s==null||s===""||s==="null")c.delete(o);else c.set(o,s)};i(t.q,r.q||null);i(t.page,String(r.page));i(t.limit,String(r.limit));i(t.sort,r.sort??null);try{const o=JSON.stringify(r.filters??{});i(t.filters,o==="{}"?null:o)}catch{}const f=`${e.pathname}?${c.toString()}${e.hash}`;if(d)window.history.replaceState({},"",f);else window.history.pushState({},"",f);F(r)},[g,t,d]),S=a.useCallback((l,e)=>{u({filters:{...g.filters,[l]:e}})},[g.filters,u]),T=a.useCallback(()=>{u({filters:{},page:1})},[u]);a.useEffect(()=>{const l=()=>F(w());window.addEventListener("popstate",l);return()=>window.removeEventListener("popstate",l)},[w]);return{state:g,setState:u,setFilter:S,clearFilters:T}}
1
+ import{createInitialListFilters as N,sanitizeListUserFilters as _}from"@contractspec/lib.presentation-runtime-core";import*as Q from"react";export function useListUrlState({defaults:E,paramKeys:h={q:"q",page:"page",limit:"limit",sort:"sort",filters:"f"},replaceState:$=!0,filterScope:J}){const Y=Q.useMemo(()=>({...N(J),..._(E.filters,J)}),[E.filters,J]),Z=Q.useCallback(()=>{if(typeof window>"u")return E;const A=new URL(window.location.href).searchParams,V=(X,M)=>{const H=X?Number(X):NaN;return Number.isFinite(H)&&H>0?H:M},C=A.get(h.filters);let B=E.filters;if(C)try{B=_(JSON.parse(C),J)}catch{B=Y}else B=Y;return{q:A.get(h.q)??E.q,page:V(A.get(h.page),E.page),limit:V(A.get(h.limit),E.limit),sort:A.get(h.sort),filters:B}},[Y,E,J,h]),[T,b]=Q.useState(Z),W=Q.useCallback((G)=>{if(typeof window>"u")return;const A=new URL(window.location.href),V=A.searchParams,C={...T,...G,filters:_(G.filters??T.filters,J)},B=(M,H)=>{if(H==null||H===""||H==="null")V.delete(M);else V.set(M,H)};B(h.q,C.q||null);B(h.page,String(C.page));B(h.limit,String(C.limit));B(h.sort,C.sort??null);try{const M=JSON.stringify(C.filters??{});B(h.filters,M==="{}"?null:M)}catch{}const X=`${A.pathname}?${V.toString()}${A.hash}`;if($)window.history.replaceState({},"",X);else window.history.pushState({},"",X);b(C)},[J,T,h,$]),I=Q.useCallback((G,A)=>{W({filters:{...T.filters,[G]:A}})},[T.filters,W]),j=Q.useCallback(()=>{W({filters:{},page:1})},[W]);Q.useEffect(()=>{const G=()=>b(Z());window.addEventListener("popstate",G);return()=>window.removeEventListener("popstate",G)},[Z]);return{state:T,setState:W,setFilter:I,clearFilters:j}}
@@ -1 +1 @@
1
- import{jsx as G,jsxs as w,Fragment as d}from"react/jsx-runtime";import{Pagination as b}from"@contractspec/lib.ui-kit-web/ui/atoms/Pagination";import{VStack as j}from"@contractspec/lib.ui-kit-web/ui/stack";import{Text as _}from"@contractspec/lib.ui-kit-web/ui/text";import*as k from"react";import{resolveTranslationString as F,useDesignSystemTranslation as N}from"../../i18n/translation";import{FiltersToolbar as x}from"../molecules/FiltersToolbar";import{DataViewDetail as A}from"./DataViewDetail";import{DataViewList as T}from"./DataViewList";import{DataViewTable as C}from"./DataViewTable";export function DataViewRenderer({spec:E,items:M=[],item:Y=null,className:I,renderActions:Q,onSelect:U,onRowClick:Z,toolbar:$,loading:q,headerActions:O,emptyState:K,footer:z,search:V,onSearchChange:L,filters:J,onFilterChange:P,pagination:H,onPageChange:R}){const W=N(),u=k.useMemo(()=>{switch(E.view.kind){case"list":return G(T,{spec:E,items:M,className:I,renderActions:Q,onSelect:U,emptyState:K});case"table":return G(C,{spec:E,items:M,className:I,onRowClick:Z,toolbar:$,loading:q,emptyState:K,headerActions:O,footer:z});case"detail":return G(A,{spec:E,item:Y,className:I,emptyState:K,headerActions:O});case"grid":{const B=E.view,X={kind:"list",layout:"card",fields:B.fields,filters:B.filters,actions:B.actions,primaryField:B.primaryField,secondaryFields:B.secondaryFields},v={...E,view:X};return G(T,{spec:v,items:M,className:I,renderActions:Q,onSelect:U,emptyState:K})}default:return G(_,{className:I,children:F("Unsupported data view kind",W)})}},[E,M,Y,I,Q,U,Z,$,q,O,K,z,W]);if(!(E.view.kind==="list"||E.view.kind==="table"||E.view.kind==="grid"))return G(d,{children:u});return w(j,{gap:"lg",children:[(E.view.filters?.length||L)&&G(x,{searchValue:V,onSearchChange:L,searchPlaceholder:F("Search...",W)??"Search...",activeChips:J?Object.entries(J).map(([B,X])=>({key:B,label:`${B}: ${X}`,onRemove:()=>{if(J){const{[B]:v,...D}=J;P?.(D)}}})):[],onClearAll:J&&Object.keys(J).length>0?()=>P?.({}):void 0,right:E.view.kind==="table"?void 0:O}),u,H&&H.total>0&&G(b,{currentPage:H.page,totalPages:Math.ceil(H.total/H.pageSize),totalItems:H.total,itemsPerPage:H.pageSize,onPageChange:(B)=>R?.(B),onItemsPerPageChange:(B)=>{R?.(1)},showItemsPerPage:!1})]})}
1
+ import{jsx as J,jsxs as c,Fragment as p}from"react/jsx-runtime";import{resolveDataViewFilters as S}from"@contractspec/lib.contracts-spec/data-views";import{Pagination as d}from"@contractspec/lib.ui-kit-web/ui/atoms/Pagination";import{VStack as h}from"@contractspec/lib.ui-kit-web/ui/stack";import{Text as f}from"@contractspec/lib.ui-kit-web/ui/text";import*as L from"react";import{resolveTranslationString as V,useDesignSystemTranslation as u}from"../../i18n/translation";import{FiltersToolbar as m}from"../molecules/FiltersToolbar";import{DataViewDetail as v}from"./DataViewDetail";import{DataViewList as b}from"./DataViewList";import{DataViewTable as y}from"./DataViewTable";export function DataViewRenderer({spec:B,items:G=[],item:X=null,className:M,renderActions:$,onSelect:q,onRowClick:P,toolbar:R,loading:T,headerActions:Y,emptyState:Q,footer:j,search:w,onSearchChange:_,filters:I,onFilterChange:Z,pagination:K,onPageChange:D}){const z=u(),O=L.useMemo(()=>S({filters:B.view.filters,scope:B.view.filterScope,user:g(I)}),[I,B.view.filterScope,B.view.filters]),N=L.useMemo(()=>{if(B.view.filterScope){const E=Object.entries(O.user).map(([H,W])=>({key:H,label:`${A(B,H)}: ${F(W)}`,onRemove:()=>{const{[H]:r,...C}=O.user;Z?.(C)}})),U=O.lockedChips==="hidden"?[]:Object.entries(O.locked).map(([H,W])=>({key:`locked-${H}`,label:`${A(B,H)}: ${F(W)}`,disabled:!0}));return[...E,...U]}return I?Object.entries(I).map(([E,U])=>({key:E,label:`${E}: ${String(U)}`,onRemove:()=>{const{[E]:H,...W}=I;Z?.(W)}})):[]},[I,Z,O,B]),k=B.view.filterScope?Object.keys(O.user).length>0:Boolean(I&&Object.keys(I).length>0),x=L.useMemo(()=>{switch(B.view.kind){case"list":return J(b,{spec:B,items:G,className:M,renderActions:$,onSelect:q,emptyState:Q});case"table":return J(y,{spec:B,items:G,className:M,onRowClick:P,toolbar:R,loading:T,emptyState:Q,headerActions:Y,footer:j});case"detail":return J(v,{spec:B,item:X,className:M,emptyState:Q,headerActions:Y});case"grid":{const E=B.view,U={kind:"list",layout:"card",fields:E.fields,filters:E.filters,actions:E.actions,primaryField:E.primaryField,secondaryFields:E.secondaryFields,filterScope:E.filterScope},H={...B,view:U};return J(b,{spec:H,items:G,className:M,renderActions:$,onSelect:q,emptyState:Q})}default:return J(f,{className:M,children:V("Unsupported data view kind",z)})}},[B,G,X,M,$,q,P,R,T,Y,Q,j,z]);if(!(B.view.kind==="list"||B.view.kind==="table"||B.view.kind==="grid"))return J(p,{children:x});return c(h,{gap:"lg",children:[(B.view.filters?.length||_||N.length)&&J(m,{searchValue:w,onSearchChange:_,searchPlaceholder:V("Search...",z)??"Search...",activeChips:N,onClearAll:k?()=>Z?.({}):void 0,right:B.view.kind==="table"?void 0:Y}),x,K&&K.total>0&&J(d,{currentPage:K.page,totalPages:Math.ceil(K.total/K.pageSize),totalItems:K.total,itemsPerPage:K.pageSize,onPageChange:(E)=>D?.(E),onItemsPerPageChange:(E)=>{D?.(1)},showItemsPerPage:!1})]})}function g(B){if(!B)return;return Object.fromEntries(Object.entries(B??{}).filter((G)=>Boolean(G[1]&&typeof G[1]==="object"&&"kind"in G[1]&&typeof G[1].kind==="string")))}function A(B,G){return B.view.filters?.find((X)=>X.key===G)?.label??G}function F(B){if(!B)return"";if(B.kind==="single")return String(B.value);if(B.kind==="multi")return B.values.map(String).join(", ");if(B.kind==="range")return[B.from==null?"":String(B.from),B.to==null?"":String(B.to)].filter(Boolean).join(" - ");return`${B.mode}(${B.clauses.length})`}
@@ -1 +1 @@
1
- import{jsx as G,jsxs as d,Fragment as w}from"react/jsx-runtime";import{Pagination as D}from"@contractspec/lib.ui-kit/ui/atoms/Pagination";import{VStack as j}from"@contractspec/lib.ui-kit/ui/stack";import{Text as _}from"@contractspec/lib.ui-kit/ui/text";import*as k from"react";import{resolveTranslationString as F,useDesignSystemTranslation as N}from"../../i18n/translation";import{FiltersToolbar as x}from"../molecules/FiltersToolbar";import{DataViewDetail as A}from"./DataViewDetail";import{DataViewList as T}from"./DataViewList";import{DataViewTable as C}from"./DataViewTable";export function DataViewRenderer({spec:E,items:M=[],item:Y=null,className:I,renderActions:Q,onSelect:U,onRowClick:Z,toolbar:$,loading:q,headerActions:O,emptyState:K,footer:z,search:V,onSearchChange:L,filters:J,onFilterChange:P,pagination:H,onPageChange:R}){const W=N(),u=k.useMemo(()=>{switch(E.view.kind){case"list":return G(T,{spec:E,items:M,className:I,renderActions:Q,onSelect:U,emptyState:K});case"table":return G(C,{spec:E,items:M,className:I,onRowClick:Z,toolbar:$,loading:q,emptyState:K,headerActions:O,footer:z});case"detail":return G(A,{spec:E,item:Y,className:I,emptyState:K,headerActions:O});case"grid":{const B=E.view,X={kind:"list",layout:"card",fields:B.fields,filters:B.filters,actions:B.actions,primaryField:B.primaryField,secondaryFields:B.secondaryFields},v={...E,view:X};return G(T,{spec:v,items:M,className:I,renderActions:Q,onSelect:U,emptyState:K})}default:return G(_,{className:I,children:F("Unsupported data view kind",W)})}},[E,M,Y,I,Q,U,Z,$,q,O,K,z,W]);if(!(E.view.kind==="list"||E.view.kind==="table"||E.view.kind==="grid"))return G(w,{children:u});return d(j,{gap:"lg",children:[(E.view.filters?.length||L)&&G(x,{searchValue:V,onSearchChange:L,searchPlaceholder:F("Search...",W)??"Search...",activeChips:J?Object.entries(J).map(([B,X])=>({key:B,label:`${B}: ${X}`,onRemove:()=>{if(J){const{[B]:v,...b}=J;P?.(b)}}})):[],onClearAll:J&&Object.keys(J).length>0?()=>P?.({}):void 0,right:E.view.kind==="table"?void 0:O}),u,H&&H.total>0?G(D,{currentPage:H.page,totalPages:Math.ceil(H.total/H.pageSize),totalItems:H.total,itemsPerPage:H.pageSize,onPageChange:(B)=>R?.(B),onItemsPerPageChange:(B)=>{R?.(1)},showItemsPerPage:!1}):null]})}
1
+ import{jsx as J,jsxs as c,Fragment as p}from"react/jsx-runtime";import{resolveDataViewFilters as S}from"@contractspec/lib.contracts-spec/data-views";import{Pagination as d}from"@contractspec/lib.ui-kit/ui/atoms/Pagination";import{VStack as h}from"@contractspec/lib.ui-kit/ui/stack";import{Text as f}from"@contractspec/lib.ui-kit/ui/text";import*as L from"react";import{resolveTranslationString as x,useDesignSystemTranslation as u}from"../../i18n/translation";import{FiltersToolbar as m}from"../molecules/FiltersToolbar";import{DataViewDetail as v}from"./DataViewDetail";import{DataViewList as V}from"./DataViewList";import{DataViewTable as y}from"./DataViewTable";export function DataViewRenderer({spec:B,items:G=[],item:X=null,className:M,renderActions:$,onSelect:q,onRowClick:P,toolbar:R,loading:T,headerActions:Y,emptyState:Q,footer:j,search:w,onSearchChange:_,filters:I,onFilterChange:Z,pagination:K,onPageChange:D}){const z=u(),O=L.useMemo(()=>S({filters:B.view.filters,scope:B.view.filterScope,user:g(I)}),[I,B.view.filterScope,B.view.filters]),N=L.useMemo(()=>{if(B.view.filterScope){const E=Object.entries(O.user).map(([H,W])=>({key:H,label:`${A(B,H)}: ${F(W)}`,onRemove:()=>{const{[H]:r,...C}=O.user;Z?.(C)}})),U=O.lockedChips==="hidden"?[]:Object.entries(O.locked).map(([H,W])=>({key:`locked-${H}`,label:`${A(B,H)}: ${F(W)}`,disabled:!0}));return[...E,...U]}return I?Object.entries(I).map(([E,U])=>({key:E,label:`${E}: ${String(U)}`,onRemove:()=>{const{[E]:H,...W}=I;Z?.(W)}})):[]},[I,Z,O,B]),k=B.view.filterScope?Object.keys(O.user).length>0:Boolean(I&&Object.keys(I).length>0),b=L.useMemo(()=>{switch(B.view.kind){case"list":return J(V,{spec:B,items:G,className:M,renderActions:$,onSelect:q,emptyState:Q});case"table":return J(y,{spec:B,items:G,className:M,onRowClick:P,toolbar:R,loading:T,emptyState:Q,headerActions:Y,footer:j});case"detail":return J(v,{spec:B,item:X,className:M,emptyState:Q,headerActions:Y});case"grid":{const E=B.view,U={kind:"list",layout:"card",fields:E.fields,filters:E.filters,actions:E.actions,primaryField:E.primaryField,secondaryFields:E.secondaryFields,filterScope:E.filterScope},H={...B,view:U};return J(V,{spec:H,items:G,className:M,renderActions:$,onSelect:q,emptyState:Q})}default:return J(f,{className:M,children:x("Unsupported data view kind",z)})}},[B,G,X,M,$,q,P,R,T,Y,Q,j,z]);if(!(B.view.kind==="list"||B.view.kind==="table"||B.view.kind==="grid"))return J(p,{children:b});return c(h,{gap:"lg",children:[(B.view.filters?.length||_||N.length)&&J(m,{searchValue:w,onSearchChange:_,searchPlaceholder:x("Search...",z)??"Search...",activeChips:N,onClearAll:k?()=>Z?.({}):void 0,right:B.view.kind==="table"?void 0:Y}),b,K&&K.total>0?J(d,{currentPage:K.page,totalPages:Math.ceil(K.total/K.pageSize),totalItems:K.total,itemsPerPage:K.pageSize,onPageChange:(E)=>D?.(E),onItemsPerPageChange:(E)=>{D?.(1)},showItemsPerPage:!1}):null]})}function g(B){if(!B)return;return Object.fromEntries(Object.entries(B??{}).filter((G)=>Boolean(G[1]&&typeof G[1]==="object"&&"kind"in G[1]&&typeof G[1].kind==="string")))}function A(B,G){return B.view.filters?.find((X)=>X.key===G)?.label??G}function F(B){if(!B)return"";if(B.kind==="single")return String(B.value);if(B.kind==="multi")return B.values.map(String).join(", ");if(B.kind==="range")return[B.from==null?"":String(B.from),B.to==null?"":String(B.to)].filter(Boolean).join(" - ");return`${B.mode}(${B.clauses.length})`}
@@ -1 +1 @@
1
- import{jsx as E,jsxs as D}from"react/jsx-runtime";import{Badge as k}from"@contractspec/lib.ui-kit-web/ui/badge";import{cn as I}from"@contractspec/lib.ui-kit-web/ui/utils";import*as K from"react";import{Button as W}from"../atoms/Button";import{Input as N}from"../atoms/Input";export function FiltersToolbar({className:X,children:Y,right:Z,searchPlaceholder:$,searchValue:G,onSearchChange:F,onSearchSubmit:L,debounceMs:O=250,activeChips:T=[],onClearAll:H}){const[J,U]=K.useState(G??"");K.useEffect(()=>{U(G??"")},[G]);K.useEffect(()=>{if(!F)return;const z=setTimeout(()=>F(J),O);return()=>clearTimeout(z)},[J,O,F]);return D("div",{className:I("space-y-2",X),children:[D("div",{className:"flex flex-col items-stretch gap-2 md:flex-row md:items-center md:justify-between",children:[D("div",{className:"flex flex-1 items-center gap-2",children:[F?D("div",{className:"flex flex-1 items-center gap-2",children:[E(N,{value:J,onChange:(z)=>U(z.target.value),onKeyDown:(z)=>{if(z.key==="Enter")L?.()},placeholder:$,keyboard:{kind:"search"}}),E(W,{variant:"outline",onPress:()=>L?.(),className:"shrink-0",children:"Rechercher"})]}):null,Y]}),Z]}),(T.length>0||H)&&D("div",{className:"flex flex-wrap items-center gap-2",children:[T.map((z)=>D(k,{variant:"secondary",className:"inline-flex items-center gap-2",children:[E("span",{children:z.label}),z.onRemove&&E("button",{type:"button","aria-label":"Supprimer le filtre",onClick:z.onRemove,className:"rounded-xs px-1 text-base hover:bg-muted",children:"×"})]},z.key)),H&&E(W,{size:"sm",variant:"ghost",onPress:H,children:"Effacer les filtres"})]})]})}
1
+ import{jsx as E,jsxs as D}from"react/jsx-runtime";import{Badge as I}from"@contractspec/lib.ui-kit-web/ui/badge";import{cn as W}from"@contractspec/lib.ui-kit-web/ui/utils";import*as K from"react";import{Button as X}from"../atoms/Button";import{Input as N}from"../atoms/Input";export function FiltersToolbar({className:Y,children:Z,right:$,searchPlaceholder:k,searchValue:G,onSearchChange:F,onSearchSubmit:L,debounceMs:O=250,activeChips:T=[],onClearAll:H}){const[J,U]=K.useState(G??"");K.useEffect(()=>{U(G??"")},[G]);K.useEffect(()=>{if(!F)return;const z=setTimeout(()=>F(J),O);return()=>clearTimeout(z)},[J,O,F]);return D("div",{className:W("space-y-2",Y),children:[D("div",{className:"flex flex-col items-stretch gap-2 md:flex-row md:items-center md:justify-between",children:[D("div",{className:"flex flex-1 items-center gap-2",children:[F?D("div",{className:"flex flex-1 items-center gap-2",children:[E(N,{value:J,onChange:(z)=>U(z.target.value),onKeyDown:(z)=>{if(z.key==="Enter")L?.()},placeholder:k,keyboard:{kind:"search"}}),E(X,{variant:"outline",onPress:()=>L?.(),className:"shrink-0",children:"Rechercher"})]}):null,Z]}),$]}),(T.length>0||H)&&D("div",{className:"flex flex-wrap items-center gap-2",children:[T.map((z)=>D(I,{variant:"secondary",className:W("inline-flex items-center gap-2",z.disabled&&"opacity-70"),children:[E("span",{children:z.label}),z.onRemove&&!z.disabled&&E("button",{type:"button","aria-label":"Supprimer le filtre",onClick:z.onRemove,className:"rounded-xs px-1 text-base hover:bg-muted",children:"×"})]},z.key)),H&&E(X,{size:"sm",variant:"ghost",onPress:H,children:"Effacer les filtres"})]})]})}
@@ -1 +1 @@
1
- import{jsx as E,jsxs as F}from"react/jsx-runtime";import{HStack as L,VStack as P}from"@contractspec/lib.ui-kit/ui/stack";import*as O from"react";import{Button as T}from"../atoms/Button";import{Input as Q}from"../atoms/Input";export function FiltersToolbar({className:Y,children:Z,right:$,searchPlaceholder:I,searchValue:G,onSearchChange:D,onSearchSubmit:N,debounceMs:U=250,activeChips:W=[],onClearAll:J}){const[K,X]=O.useState(G??"");O.useEffect(()=>{X(G??"")},[G]);O.useEffect(()=>{if(!D)return;const z=setTimeout(()=>D(K),U);return()=>clearTimeout(z)},[K,U,D]);return F(P,{className:Y,children:[F(L,{className:"items-center gap-2",children:[D?F(L,{className:"flex-1 items-center gap-2",children:[E(Q,{value:K,onChange:(z)=>X(z.target.value),placeholder:I,keyboard:{kind:"search"}}),E(T,{variant:"outline",onPress:()=>N?.(),children:"Rechercher"})]}):null,Z,$]}),(W.length>0||J)&&F(L,{className:"mt-2 flex flex-wrap items-center gap-2",children:[W.map((z)=>E(T,{size:"sm",variant:"secondary",onPress:z.onRemove,children:z.label},z.key)),J?E(T,{size:"sm",variant:"ghost",onPress:J,children:"Effacer les filtres"}):null]})]})}
1
+ import{jsx as E,jsxs as F}from"react/jsx-runtime";import{HStack as L,VStack as P}from"@contractspec/lib.ui-kit/ui/stack";import*as O from"react";import{Button as T}from"../atoms/Button";import{Input as Q}from"../atoms/Input";export function FiltersToolbar({className:Y,children:Z,right:$,searchPlaceholder:I,searchValue:G,onSearchChange:D,onSearchSubmit:N,debounceMs:U=250,activeChips:W=[],onClearAll:J}){const[K,X]=O.useState(G??"");O.useEffect(()=>{X(G??"")},[G]);O.useEffect(()=>{if(!D)return;const z=setTimeout(()=>D(K),U);return()=>clearTimeout(z)},[K,U,D]);return F(P,{className:Y,children:[F(L,{className:"items-center gap-2",children:[D?F(L,{className:"flex-1 items-center gap-2",children:[E(Q,{value:K,onChange:(z)=>X(z.target.value),placeholder:I,keyboard:{kind:"search"}}),E(T,{variant:"outline",onPress:()=>N?.(),children:"Rechercher"})]}):null,Z,$]}),(W.length>0||J)&&F(L,{className:"mt-2 flex flex-wrap items-center gap-2",children:[W.map((z)=>E(T,{size:"sm",variant:"secondary",disabled:z.disabled,onPress:z.disabled?void 0:z.onRemove,children:z.label},z.key)),J?E(T,{size:"sm",variant:"ghost",onPress:J,children:"Effacer les filtres"}):null]})]})}
@@ -1 +1 @@
1
- import*as a from"react";export function useListUrlState({defaults:n,paramKeys:t={q:"q",page:"page",limit:"limit",sort:"sort",filters:"f"},replaceState:d=!0}){const w=a.useCallback(()=>{if(typeof window>"u")return n;const e=new URL(window.location.href).searchParams,c=(f,o)=>{const s=f?Number(f):NaN;return Number.isFinite(s)&&s>0?s:o},r=e.get(t.filters);let i=n.filters;if(r)try{i=JSON.parse(r)}catch{i=n.filters}return{q:e.get(t.q)??n.q,page:c(e.get(t.page),n.page),limit:c(e.get(t.limit),n.limit),sort:e.get(t.sort),filters:i}},[n,t]),[g,F]=a.useState(w),u=a.useCallback((l)=>{if(typeof window>"u")return;const e=new URL(window.location.href),c=e.searchParams,r={...g,...l},i=(o,s)=>{if(s==null||s===""||s==="null")c.delete(o);else c.set(o,s)};i(t.q,r.q||null);i(t.page,String(r.page));i(t.limit,String(r.limit));i(t.sort,r.sort??null);try{const o=JSON.stringify(r.filters??{});i(t.filters,o==="{}"?null:o)}catch{}const f=`${e.pathname}?${c.toString()}${e.hash}`;if(d)window.history.replaceState({},"",f);else window.history.pushState({},"",f);F(r)},[g,t,d]),S=a.useCallback((l,e)=>{u({filters:{...g.filters,[l]:e}})},[g.filters,u]),T=a.useCallback(()=>{u({filters:{},page:1})},[u]);a.useEffect(()=>{const l=()=>F(w());window.addEventListener("popstate",l);return()=>window.removeEventListener("popstate",l)},[w]);return{state:g,setState:u,setFilter:S,clearFilters:T}}
1
+ import{createInitialListFilters as N,sanitizeListUserFilters as _}from"@contractspec/lib.presentation-runtime-core";import*as Q from"react";export function useListUrlState({defaults:E,paramKeys:h={q:"q",page:"page",limit:"limit",sort:"sort",filters:"f"},replaceState:$=!0,filterScope:J}){const Y=Q.useMemo(()=>({...N(J),..._(E.filters,J)}),[E.filters,J]),Z=Q.useCallback(()=>{if(typeof window>"u")return E;const A=new URL(window.location.href).searchParams,V=(X,M)=>{const H=X?Number(X):NaN;return Number.isFinite(H)&&H>0?H:M},C=A.get(h.filters);let B=E.filters;if(C)try{B=_(JSON.parse(C),J)}catch{B=Y}else B=Y;return{q:A.get(h.q)??E.q,page:V(A.get(h.page),E.page),limit:V(A.get(h.limit),E.limit),sort:A.get(h.sort),filters:B}},[Y,E,J,h]),[T,b]=Q.useState(Z),W=Q.useCallback((G)=>{if(typeof window>"u")return;const A=new URL(window.location.href),V=A.searchParams,C={...T,...G,filters:_(G.filters??T.filters,J)},B=(M,H)=>{if(H==null||H===""||H==="null")V.delete(M);else V.set(M,H)};B(h.q,C.q||null);B(h.page,String(C.page));B(h.limit,String(C.limit));B(h.sort,C.sort??null);try{const M=JSON.stringify(C.filters??{});B(h.filters,M==="{}"?null:M)}catch{}const X=`${A.pathname}?${V.toString()}${A.hash}`;if($)window.history.replaceState({},"",X);else window.history.pushState({},"",X);b(C)},[J,T,h,$]),I=Q.useCallback((G,A)=>{W({filters:{...T.filters,[G]:A}})},[T.filters,W]),j=Q.useCallback(()=>{W({filters:{},page:1})},[W]);Q.useEffect(()=>{const G=()=>b(Z());window.addEventListener("popstate",G);return()=>window.removeEventListener("popstate",G)},[Z]);return{state:T,setState:W,setFilter:I,clearFilters:j}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contractspec/lib.design-system",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Design tokens and theming primitives",
5
5
  "keywords": [
6
6
  "contractspec",
@@ -31,12 +31,12 @@
31
31
  "sideEffects": false,
32
32
  "tree-shake": true,
33
33
  "dependencies": {
34
- "@contractspec/lib.ai-agent": "8.0.11",
35
- "@contractspec/lib.contracts-spec": "5.6.0",
36
- "@contractspec/lib.contracts-runtime-client-react": "3.11.0",
37
- "@contractspec/lib.presentation-runtime-react": "38.0.3",
38
- "@contractspec/lib.ui-kit": "4.1.0",
39
- "@contractspec/lib.ui-kit-web": "3.12.0",
34
+ "@contractspec/lib.ai-agent": "8.0.12",
35
+ "@contractspec/lib.contracts-spec": "5.7.0",
36
+ "@contractspec/lib.contracts-runtime-client-react": "3.11.1",
37
+ "@contractspec/lib.presentation-runtime-react": "39.0.0",
38
+ "@contractspec/lib.ui-kit": "4.1.1",
39
+ "@contractspec/lib.ui-kit-web": "3.12.1",
40
40
  "@hookform/resolvers": "5.2.2",
41
41
  "class-variance-authority": "^0.7.1",
42
42
  "clsx": "^2.1.1",