@evotecit/canopyx 0.3.5 → 0.3.7

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.
Files changed (58) hide show
  1. package/CanopyX.Html/Assets/canopyx-calendar.js +4 -3
  2. package/CanopyX.Html/Assets/canopyx-detail-rows.js +2 -2
  3. package/CanopyX.Html/Assets/canopyx-filter-editor.js +2 -1
  4. package/CanopyX.Html/Assets/canopyx-flow-checks.css +24 -0
  5. package/CanopyX.Html/Assets/canopyx-flow-checks.js +13 -0
  6. package/CanopyX.Html/Assets/canopyx-flow-command.css +14 -0
  7. package/CanopyX.Html/Assets/canopyx-flow-command.js +19 -0
  8. package/CanopyX.Html/Assets/canopyx-flow-consent.css +6 -0
  9. package/CanopyX.Html/Assets/canopyx-flow-consent.js +8 -0
  10. package/CanopyX.Html/Assets/canopyx-flow-dialog.css +14 -0
  11. package/CanopyX.Html/Assets/canopyx-flow-dialog.js +3 -0
  12. package/CanopyX.Html/Assets/canopyx-flow-drafts.css +17 -0
  13. package/CanopyX.Html/Assets/canopyx-flow-drafts.js +21 -0
  14. package/CanopyX.Html/Assets/canopyx-flow-endpoint.js +6 -0
  15. package/CanopyX.Html/Assets/canopyx-flow-entry.css +1 -0
  16. package/CanopyX.Html/Assets/canopyx-flow-entry.js +6 -2
  17. package/CanopyX.Html/Assets/canopyx-flow-host.css +5 -0
  18. package/CanopyX.Html/Assets/canopyx-flow-host.js +4 -0
  19. package/CanopyX.Html/Assets/canopyx-flow-links.js +6 -0
  20. package/CanopyX.Html/Assets/canopyx-flow-module.mjs +6 -2
  21. package/CanopyX.Html/Assets/canopyx-flow-options.js +25 -0
  22. package/CanopyX.Html/Assets/canopyx-flow-pick.css +19 -0
  23. package/CanopyX.Html/Assets/canopyx-flow-pick.js +9 -0
  24. package/CanopyX.Html/Assets/canopyx-flow-preview.css +11 -0
  25. package/CanopyX.Html/Assets/canopyx-flow-preview.js +9 -0
  26. package/CanopyX.Html/Assets/canopyx-flow-records.css +9 -0
  27. package/CanopyX.Html/Assets/canopyx-flow-records.js +12 -0
  28. package/CanopyX.Html/Assets/canopyx-flow-tasks.css +29 -0
  29. package/CanopyX.Html/Assets/canopyx-flow-tasks.js +20 -0
  30. package/CanopyX.Html/Assets/canopyx-flow.js +48 -45
  31. package/CanopyX.Html/Assets/canopyx-formatting.js +7 -6
  32. package/CanopyX.Html/Assets/canopyx-grid-module.mjs +69 -42
  33. package/CanopyX.Html/Assets/canopyx-grid.css +23 -0
  34. package/CanopyX.Html/Assets/canopyx-grid.js +69 -42
  35. package/CanopyX.Html/Assets/canopyx-interactive.js +4 -4
  36. package/CanopyX.Html/Assets/canopyx-labels-de.js +77 -1
  37. package/CanopyX.Html/Assets/canopyx-labels-es.js +77 -1
  38. package/CanopyX.Html/Assets/canopyx-labels-fr.js +77 -1
  39. package/CanopyX.Html/Assets/canopyx-labels-it.js +77 -1
  40. package/CanopyX.Html/Assets/canopyx-labels-pl.js +77 -1
  41. package/CanopyX.Html/Assets/canopyx-live.js +4 -3
  42. package/CanopyX.Html/Assets/canopyx-matrix.js +3 -3
  43. package/CanopyX.Html/Assets/canopyx-module.mjs +4 -4
  44. package/CanopyX.Html/Assets/canopyx-reporting-module.mjs +71 -44
  45. package/CanopyX.Html/Assets/canopyx-reporting.css +23 -0
  46. package/CanopyX.Html/Assets/canopyx-reporting.js +71 -44
  47. package/CanopyX.Html/Assets/canopyx-tree-tools.js +10 -8
  48. package/CanopyX.Html/Assets/canopyx.js +4 -4
  49. package/JavaScript/flow.d.ts +180 -9
  50. package/JavaScript/grid.d.ts +8 -0
  51. package/JavaScript/locales/de.mjs +77 -1
  52. package/JavaScript/locales/es.mjs +77 -1
  53. package/JavaScript/locales/fr.mjs +77 -1
  54. package/JavaScript/locales/it.mjs +77 -1
  55. package/JavaScript/locales/pl.mjs +77 -1
  56. package/JavaScript/protocol.d.ts +1 -1
  57. package/README.md +6 -2
  58. package/package.json +3 -1
@@ -0,0 +1,19 @@
1
+ /* The flow-pick part: the view,and the tray beside it on wide forms. */
2
+ .cx-flow{--cx-size-260px:260px;}
3
+ .cx-flow-type-pick{max-width:none;}
4
+ .cx-flow .cx-flow-label-quiet{position:absolute;width:var(--cx-size-1px);height:var(--cx-size-1px);overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;}
5
+ .cx-flow-label-quiet + .cx-flow-help{margin-top:var(--cx-size-4px);}
6
+ .cx-flow-step:has(.cx-flow-type-pick){max-width:none;}
7
+ .cx-flow-pick{display:grid;gap:var(--cx-size-12px);}
8
+ @container (min-width:860px){.cx-flow-pick.cx-flow-pick-tray{grid-template-columns:minmax(0,1fr) var(--cx-size-260px);align-items:start;}}
9
+ .cx-flow-pick-view{min-width:0;border:var(--cx-size-1px) solid var(--cx-line);border-radius:var(--cx-radius);overflow:hidden;}
10
+ .cx-flow-pick-view > .cx-host{height:100%;}
11
+ .cx-flow-tray{display:flex;flex-direction:column;gap:var(--cx-size-6px);padding:var(--cx-size-10px) var(--cx-size-12px);border:var(--cx-size-1px) solid var(--cx-line);border-radius:var(--cx-radius);background:var(--cx-head-bg);}
12
+ .cx-flow-tray-head{display:flex;align-items:baseline;justify-content:space-between;gap:var(--cx-size-8px);}
13
+ .cx-flow-tray-title{font-size:var(--cx-size-13d5px);font-weight:600;}
14
+ .cx-flow-tray-list{list-style:none;padding:0;display:flex;flex-direction:column;gap:var(--cx-size-4px);max-height:var(--cx-size-320px);overflow:auto;}
15
+ .cx-flow-tray-item{display:flex;align-items:center;gap:var(--cx-size-6px);min-height:var(--cx-size-28px);padding-inline-start:var(--cx-size-8px);border-radius:var(--cx-radius-sm);background:var(--cx-surface);}
16
+ .cx-flow-tray-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--cx-size-13d5px);}
17
+ .cx-flow-tray .cx-flow-icon-btn{width:var(--cx-size-28px);height:var(--cx-size-28px);border-color:transparent;background:transparent;font-size:var(--cx-size-16px);}
18
+ .cx-flow-tray-more,.cx-flow-tray-empty{font-size:var(--cx-size-13px);color:var(--cx-muted);}
19
+ @media print{.cx-flow-tray .cx-flow-icon-btn{display:none;}}
@@ -0,0 +1,9 @@
1
+ (function(global){"use strict";const installed=new WeakSet();const part={version:"426718e2fa1f3cd0",requires:["flow","flow-host"],install(context){if(installed.has(context.FLOW_IMPL))return;const{FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,numberFormat,FLOW_VIEWS}=context;const{FlowView,FLOW_HOST,FLOW_LABELS,flowFail,flowIs,flowKeys,flowName,flowString,flowFlag,flowScalar,flowFits,flowField,flowOptions,flowCondition,flowRefs,flowTest,flowCopy,flowSameValue,flowEmpty,flowNormal,flowDate,flowFieldView}=FLOW_IMPL.core;const{EDIT_EDITORS,formRuleSpec,formCheck}=FORMS_IMPL;const FLOW_PART_LABELS={flowPickMore:"and {count} more",flowPickCount:"[one]{count} chosen[other]{count} chosen",flowPickChosen:"Chosen",flowPickNone:"Nothing chosen yet.",flowPickOneByOne:"Choose the records one by one or a page at a time."};Object.assign(FLOW_LABELS,FLOW_PART_LABELS);const FLOW_PICK_VISIBLE=60;FLOW_HOST.pick={read(raw,step,model){flowKeys(raw,["field","view","label","tray","height"],"The pick of step "+step.id);if(!flowIs(raw.view)||raw.view.protocol!=="canopyx/2"||!["grid","view"].includes(raw.view.type))flowFail("the pick of step "+step.id+" needs a grid or tree view document");if(raw.tray!==undefined&&typeof raw.tray!=="boolean")flowFail("the tray of step "+step.id+" is true or false");flowKeys(raw.field,["id","label","help","required","when","parameter","path","sensitive","validation","default"],"The field of pick step "+step.id);const field=flowField(Object.assign({},raw.field,{type:"pick"}),step,null,model),columns=Array.isArray(raw.view.columns)?raw.view.columns:[];const label=raw.label??(columns[0]&&columns[0].id);if(typeof label!=="string"||!columns.some(column=>column&&column.id===label))flowFail("the pick of step "+step.id+" needs a label column the view declares");if(raw.height!==undefined&&(!Number.isInteger(raw.height)||raw.height<160||raw.height>1200))flowFail("the pick of step "+step.id+" needs a height of 160 to 1200 pixels");step.pick=Object.freeze({view:raw.view,kind:raw.view.type==="grid"?"grid":"tree",label,tray:raw.tray!==false,height:raw.height??420,field});step.blocks.push({section:null,fields:[field]})},built(view,step,section){if(step.pick?.field.label===step.title)section.querySelector(".cx-flow-label")?.classList.add("cx-flow-label-quiet")},input:(context,field,view)=>flowPickInput(context,field,view),fits:(field,value)=>Array.isArray(value)&&value.length<=1000&&value.every(item=>flowIs(item)&&Object.keys(item).every(key=>key==="id"||key==="label")&&
2
+ typeof item.id==="string"&&item.id.length>0&&item.id.length<=256&&typeof item.label==="string"&&item.label.length<=400)&&new Set(value.map(item=>item.id)).size===value.length,display:(view,value)=>value.slice(0,12).map(item=>item.label||item.id).join("\n")+(value.length>12?"\n"+fill(view.labels.flowPickMore,{count:value.length-12}):"")};const flowAnswer=FlowView.prototype.read;Object.assign(FlowView.prototype,{read(id){const value=flowAnswer.call(this,id);return this.model.fields.get(id)?.type==="pick"&&Array.isArray(value)?value.map(item=>item.id):value}});function flowPickText(cell,fallback){if(cell!==null&&typeof cell==="object")return typeof cell.text==="string"?cell.text:cell.value==null?fallback:String(cell.value);return cell==null?fallback:String(cell)}
3
+ function flowPickInput(context,field,view){const pick=field.step.pick,labels=view.labels,box=h("div","cx-flow-pick" +(pick.tray?" cx-flow-pick-tray":""));let items=(Array.isArray(context.value)?context.value:[]).map(item=>({id:item.id,label:item.label}));let picker=view.pickers.get(field.id);if(!picker)picker=flowPickView(view,field,pick);const changed=()=>box.dispatchEvent(new Event("change",{bubbles:true}));const tray=pick.tray?h("section","cx-flow-tray"):null,trayList=h("ul","cx-flow-tray-list"),trayHead=h("h4","cx-flow-tray-title"),clear=h("button","cx-flow-link");const count=h("p","cx-flow-count");count.setAttribute("aria-live","polite");const paint=()=>{setText(count,fill(plural(labels.flowPickCount,items.length,view.lang),{count:numberFormat(view.lang,0,0).format(items.length)}));if(!tray)return;trayList.replaceChildren(...items.slice(0,FLOW_PICK_VISIBLE).map(item=>{const row=h("li","cx-flow-tray-item"),text=h("span","cx-flow-tray-label"),remove=h("button","cx-flow-icon-btn");setText(text,item.label||item.id);text.title=item.label||item.id;remove.type="button";setText(remove,"×");remove.hidden=context.readOnly;remove.setAttribute("aria-label",fill(labels.flowRemoveItem,{item:item.label||item.id}));remove.addEventListener("click",()=>{const at=items.findIndex(entry=>entry.id===item.id);items=items.filter(entry=>entry.id!==item.id);picker.select(items.map(entry=>entry.id));paint();changed();(trayList.children[Math.min(at,trayList.children.length-1)]?.querySelector("button")||clear).focus()});row.append(text,remove);return row}));if(items.length>FLOW_PICK_VISIBLE){const more=h("li","cx-flow-tray-more");setText(more,fill(labels.flowPickMore,{count:items.length-FLOW_PICK_VISIBLE}));trayList.appendChild(more)}
4
+ clear.hidden=!items.length||context.readOnly;const none=!items.length;trayList.hidden=none;count.hidden=none;tray.querySelector(".cx-flow-tray-empty").hidden=!none};if(tray){setText(trayHead,labels.flowPickChosen);clear.type="button";setText(clear,labels.flowSelectNone);clear.addEventListener("click",()=>{items=[];picker.select([]);paint();changed()});const head=h("div","cx-flow-tray-head"),empty=h("p","cx-flow-tray-empty");setText(empty,labels.flowPickNone);head.append(trayHead,clear);tray.setAttribute("aria-label",labels.flowPickChosen);tray.append(head,count,trayList,empty)}
5
+ picker.onChange=(ids,lookup)=>{const known=new Map(items.map(item=>[item.id,item]));const next=ids.slice(0,1000).map(id=>known.get(id)||{id,label:lookup(id).slice(0,400)});picker.wanted=next.map(item=>item.id);if(flowSameValue(picker.wanted,items.map(item=>item.id)))return;items=next;paint();changed()};picker.select(items.map(item=>item.id));box.append(picker.holder);if(tray)box.appendChild(tray);else box.appendChild(count);paint();return{element:box,control:picker.holder,group:true,focus:()=>picker.api?.focus?.(),getValue:()=>items.map(item=>({id:item.id,label:item.label})),destroy:()=>{picker.onChange=null}}}
6
+ function flowPickView(view,field,pick){const factory=pick.kind==="grid"?FLOW_VIEWS.grid:FLOW_VIEWS.tree;if(typeof factory!=="function")flowFail("the pick step "+field.step.id+" needs the data-grid or reporting entry point, which hold grids and trees");const holder=h("div","cx-flow-pick-view"),max=field.items.max;holder.style.height=pick.height+"px";const doc=Object.assign(flowCopy(pick.view),{selection:max===1?"single":"multiple"});if(pick.kind==="grid"&&FLOW_VIEWS.reporting)doc.reporting=Object.assign({},doc.reporting,{selectionControls:true});const picker={holder,api:null,wanted:[],onChange:null};const options=Object.assign({},view.hooks.pickers?.[field.step.id],{view:doc});if(pick.kind==="tree")delete options.onSelect;picker.api=factory(holder,options);view.pickers.set(field.id,picker);const label=pick.label;const labelsOf=(ids,nodes)=>{if(pick.kind==="grid"){const rows=new Map((picker.api.getLoadedRows?.(ids)||[]).map(row=>[row.id,row]));return id=>flowPickText(rows.get(id)?.cells?.[label],id)}
7
+ const byId=new Map(nodes.map(node=>[node.id,node]));return id=>{const node=byId.get(id);return node?flowPickText(node.cells?.[label],node.label||id):id}};holder.addEventListener("canopyx:select",event=>{event.stopPropagation();if(picker.applying)return;if(event.detail?.selection?.kind==="query"){view.announce(view.labels.flowPickOneByOne);picker.select(picker.wanted);return}
8
+ const nodes=event.detail?.nodes||[],ids=pick.kind==="grid"?[...(event.detail?.ids||[])]:nodes.map(node=>node.id);const known=new Set(picker.wanted);picker.onChange?.(ids,labelsOf(ids.filter(id=>!known.has(id)),nodes))});picker.select=ids=>{picker.wanted=ids;picker.api.ready.then(()=>{if(picker.wanted!==ids)return;picker.applying=true;try{picker.api.setSelection(ids)}catch(error){view.report(error,"picker")}finally{picker.applying=false}},error=>view.report(error,"picker"))};return picker}
9
+ installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-pick",part])})(typeof window!=="undefined"?window:globalThis);
@@ -0,0 +1,11 @@
1
+ /* The flow-preview part: the host's preview on the review page. */
2
+ .cx-flow-preview{padding:var(--cx-size-10px) var(--cx-size-14px) var(--cx-size-12px);border-radius:var(--cx-radius);border:var(--cx-size-1px) solid var(--cx-line);}
3
+ .cx-flow-preview .cx-flow-review-title{border-bottom:0;padding-bottom:0;}
4
+ .cx-flow-preview-title{margin-top:var(--cx-size-6px);font-weight:600;}
5
+ .cx-flow-preview-rows .cx-flow-answer{grid-template-columns:minmax(var(--cx-size-140px),36%) minmax(0,1fr);}
6
+ .cx-flow-preview-note{margin-top:var(--cx-size-6px);font-size:var(--cx-size-13px);color:var(--cx-text-2);}
7
+ .cx-flow-tone-success{color:var(--cx-tone-success);}
8
+ .cx-flow-tone-info{color:var(--cx-tone-info);}
9
+ .cx-flow-tone-warning{color:var(--cx-tone-warning);font-weight:600;}
10
+ .cx-flow-tone-danger{color:var(--cx-tone-danger);font-weight:600;}
11
+ .cx-flow-step > .cx-flow-preview{margin-top:var(--cx-flow-gap);}
@@ -0,0 +1,9 @@
1
+ (function(global){"use strict";const installed=new WeakSet();const part={version:"f4fb5c42743873c0",requires:["flow","flow-host"],install(context){if(installed.has(context.FLOW_IMPL))return;const{FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,numberFormat}=context;const{FlowView,FLOW_HOST,FLOW_LABELS,flowFail,flowIs,flowKeys,flowName,flowString,flowFlag,flowScalar,flowFits,flowField,flowOptions,flowCondition,flowRefs,flowTest,flowCopy,flowSameValue,flowEmpty,flowNormal,flowDate,flowFieldView}=FLOW_IMPL.core;const{EDIT_EDITORS,formRuleSpec,formCheck}=FORMS_IMPL;const FLOW_PART_LABELS={flowPreview:"Preview",flowPreviewLoading:"Loading the preview…",flowPreviewFailed:"The preview could not load: {message}",flowPreviewNone:"Nothing to preview."};Object.assign(FLOW_LABELS,FLOW_PART_LABELS);const FLOW_PREVIEW_TONES=["info","success","warning","danger"];FLOW_HOST.preview={review(view,section){if(typeof view.hooks.preview==="function")section.appendChild(view.previewBlock())}};Object.assign(FlowView.prototype,{previewBlock(){const labels=this.labels,block=h("section","cx-flow-preview"),heading=h("h4","cx-flow-review-title"),body=h("div","cx-flow-preview-body");heading.id=this.id+"-rp";setText(heading,labels.flowPreview);block.setAttribute("aria-labelledby",heading.id);block.setAttribute("aria-busy","true");const loading=h("p","cx-flow-loading");setText(loading,labels.flowPreviewLoading);body.appendChild(loading);block.append(heading,body);this.previewController?.abort();const controller=this.previewController=new AbortController();this.timed("preview",signal=>this.hooks.preview(this.getValues(),{signal}),controller).then(answer=>{if(this.previewController!==controller||this.destroyed||answer.cancelled)return;block.removeAttribute("aria-busy");if(answer.failed){const failed=h("p","cx-flow-error");setText(failed,fill(labels.flowPreviewFailed,{message:answer.failed}));body.replaceChildren(failed);return}
2
+ body.replaceChildren(...this.previewContent(answer.value))});return block},previewContent(value){const text=item=>typeof item==="string"&&/\S/.test(item) ? item.slice(0, 1000) : null, nodes = [];
3
+ const content=Array.isArray(value)?{rows:value}:flowIs(value)?value:null;if(!content){const none=h("p","cx-flow-none");setText(none,this.labels.flowPreviewNone);return[none]}
4
+ if(text(content.title)){const title=h("p","cx-flow-preview-title");setText(title,text(content.title));nodes.push(title)}
5
+ const rows=(Array.isArray(content.rows)?content.rows:[]).slice(0,50).filter(row=>flowIs(row)&&text(row.label)&&flowScalar(row.value));if(rows.length){const list=h("dl","cx-flow-answers cx-flow-preview-rows");for(const row of rows){const item=h("div","cx-flow-answer"),term=h("dt"),data=h("dd","cx-flow-answer-value");setText(term,text(row.label));setText(data,String(row.value).slice(0,1000));if(FLOW_PREVIEW_TONES.includes(row.tone))data.classList.add("cx-flow-tone-"+row.tone);item.append(term,data);list.appendChild(item)}
6
+ nodes.push(list)}
7
+ for(const note of(Array.isArray(content.notes)?content.notes:[]).slice(0,10).map(text).filter(Boolean)){const line=h("p","cx-flow-preview-note");setText(line,note);nodes.push(line)}
8
+ if(!nodes.length){const none=h("p","cx-flow-none");setText(none,this.labels.flowPreviewNone);nodes.push(none)}
9
+ return nodes}});installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-preview",part])})(typeof window!=="undefined"?window:globalThis);
@@ -0,0 +1,9 @@
1
+ /* The flow-records part: a compact table of editors. */
2
+ .cx-flow-records-scroll{max-width:100%;overflow-x:auto;}
3
+ .cx-flow-records-table{border-collapse:collapse;min-width:min(100%,var(--cx-size-440px));}
4
+ .cx-flow-records-table th{padding:0 var(--cx-size-8px) var(--cx-size-6px) 0;font-size:var(--cx-size-12px);font-weight:600;color:var(--cx-muted);text-align:start;white-space:nowrap;}
5
+ .cx-flow-records-table td{padding:0 var(--cx-size-8px) var(--cx-size-6px) 0;vertical-align:top;}
6
+ .cx-flow-records-table .cx-flow-input{min-width:var(--cx-size-92px);}
7
+ .cx-flow-records-table input[type="checkbox"].cx-flow-input{width:var(--cx-size-18px);min-width:0;height:var(--cx-flow-control);margin:0;accent-color:var(--cx-accent);}
8
+ .cx-flow-records-actions{width:var(--cx-flow-control);}
9
+ .cx-flow-records-empty{font-size:var(--cx-size-13px);color:var(--cx-muted);}
@@ -0,0 +1,12 @@
1
+ (function(global){"use strict";const installed=new WeakSet();const part={version:"f3c09661bdffb8d0",requires:["flow"],install(context){if(installed.has(context.FLOW_IMPL))return;const{FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,numberFormat}=context;const{FlowView,FLOW_HOST,FLOW_LABELS,flowFail,flowIs,flowKeys,flowName,flowString,flowFlag,flowScalar,flowFits,flowField,flowOptions,flowCondition,flowRefs,flowTest,flowCopy,flowSameValue,flowEmpty,flowNormal,flowDate,flowFieldView}=FLOW_IMPL.core;const{EDIT_EDITORS,formRuleSpec,formCheck}=FORMS_IMPL;const FLOW_PART_LABELS={flowRecordActions:"Actions",flowRecordCell:"{column}, row {row}",flowRecordRemove:"Remove row {row}",flowRecordAdd:"Add row",flowRecordsEmpty:"No rows yet.",flowRecordProblem:"Row {row}: {message}",flowRecordsCount:"[one]{count} row[other]{count} rows"};Object.assign(FLOW_LABELS,FLOW_PART_LABELS);const FLOW_RECORD_TYPES=["text","number","date","time","select","toggle"];function flowRecordColumns(raw,bad){if(!Array.isArray(raw)||!raw.length||raw.length>16)bad("needs 1 to 16 columns");const ids=new Set();return raw.map(column=>{flowKeys(column,["id","title","type","required","options","validation","width"],"A records column");const id=flowName(column.id,"A column id"),type=column.type??"text",rules=column.validation??{};if(ids.has(id))bad("has two columns named "+id);ids.add(id);if(!FLOW_RECORD_TYPES.includes(type))bad("has a column of an unknown type");flowKeys(rules,["min","max","minLength","maxLength","pattern","message"],"The validation of column "+id);if((rules.min!==undefined||rules.max!==undefined)&&type!=="number"||[rules.minLength,rules.maxLength,rules.pattern].some(value=>value!==undefined)&&type!=="text")bad("has checks its column type does not take");if((column.options!==undefined)!==(type==="select")||type==="select"&&!(Array.isArray(column.options)&&column.options.length))bad("needs options as a select column only");if(column.width!==undefined&&(!Number.isInteger(column.width)||column.width<60||column.width>480))bad("needs a column width of 60 to 480 pixels");const required=flowFlag(column.required,"required"),shared=formRuleSpec({},{required,min:rules.min,max:rules.max,minLength:rules.minLength,maxLength:rules.maxLength,pattern:rules.pattern,message:rules.message},bad);return Object.freeze({id,title:flowString(column.title,200,"A column title",true),type,required,width:column.width??null,options:type==="select"?flowOptions(column.options,bad):[],spec:Object.freeze({id,pattern:shared.pattern,rules:shared.rules})})})}
2
+ function flowCellFits(column,value){if(value===null)return true;switch(column.type){case "number":return Number.isFinite(value)&&Math.abs(value)<=Number.MAX_SAFE_INTEGER;case "toggle":return typeof value==="boolean";case "date":return flowFits({type:"date"},value);case "time":return flowFits({type:"time"},value);case "select":return column.options.some(option=>option.value===value);default:return typeof value==="string"&&value.length<=4096}}
3
+ FLOW_HOST.records={columns:flowRecordColumns,fits:(field,value)=>Array.isArray(value)&&value.length<=1000&&value.every(row=>flowIs(row)&&
4
+ Object.keys(row).every(key=>field.columns.some(column=>column.id===key))&&field.columns.every(column=>flowCellFits(column,row[column.id]??null))),input:(context,field,view)=>flowRecordsInput(context,field,view),problem:(view,field,rows)=>flowRecordsProblem(view,field,rows),display:(view,value)=>fill(plural(view.labels.flowRecordsCount,value.length,view.lang),{count:numberFormat(view.lang,0,0).format(value.length)})};function flowRecordsInput(context,field,view){const labels=view.labels,group=h("div","cx-flow-records"),table=h("table","cx-flow-records-table"),head=h("thead"),body=h("tbody");const add=h("button","cx-flow-btn cx-flow-list-add"),rows=[],max=field.items.max??1000;group.setAttribute("role","group");const headRow=h("tr");for(const column of field.columns){const th=h("th");th.scope="col";setText(th,column.title);if(column.width)th.style.width=column.width+"px";if(column.required)th.classList.add("cx-flow-records-required");headRow.appendChild(th)}
5
+ const actions=h("th","cx-flow-records-actions"),actionsText=h("span","cx-sr-only");actions.scope="col";setText(actionsText,labels.flowRecordActions);actions.appendChild(actionsText);headRow.appendChild(actions);head.appendChild(headRow);table.append(head,body);const changed=()=>group.dispatchEvent(new Event("change",{bubbles:true}));const cellText=(column,value)=>value==null?"":column.type==="number"?numberFormat(context.locale,0,20).format(value):
6
+ column.type==="select"?column.options.find(option=>option.value===value)?.label??String(value):String(value);const renumber=()=>rows.forEach((row,index)=>{row.cells.forEach(({column,instance})=>(instance.control||instance.element).setAttribute("aria-label",fill(labels.flowRecordCell,{column:column.title,row:index+1})));row.remove.setAttribute("aria-label",fill(labels.flowRecordRemove,{row:index+1}))});const paintTools=()=>{add.disabled=context.readOnly||rows.length>=max};const addRow=(values,focus)=>{const tr=h("tr","cx-flow-records-row"),row={tr,cells:[],abort:new AbortController()};for(const column of field.columns){const td=h("td"),value=values?.[column.id]??null;const cellContext=Object.freeze({id:context.id+"-r"+rows.length+"-"+column.id,value,text:cellText(column,value),readOnly:context.readOnly,row:Object.freeze({id:field.id,cells:Object.freeze({})}),column:Object.freeze({id:column.id,title:column.title,kind:column.type==="number"?"number":"text"}),editor:Object.freeze({type:column.type}),options:column.options.length?column.options:null,rules:column.spec.rules,labels,locale:context.locale,timeZone:"utc",mode:"flow",initial:undefined,signal:row.abort.signal,lookupMinLength:1,commit:changed,cancel:()=>{},lookup:()=>[],remember:changed});const instance=EDIT_EDITORS[column.type].create(cellContext),control=instance.control||instance.element;control.classList.add("cx-flow-input");if(context.readOnly){control.disabled=control.tagName==="SELECT"||control.type==="checkbox";control.readOnly=true}
7
+ td.appendChild(instance.element);tr.appendChild(td);row.cells.push({column,instance})}
8
+ const cell=h("td","cx-flow-records-actions"),remove=h("button","cx-flow-icon-btn");remove.type="button";setText(remove,"×");remove.hidden=context.readOnly;remove.addEventListener("click",()=>{const at=rows.indexOf(row);rows.splice(at,1);row.abort.abort();row.cells.forEach(({instance})=>instance.destroy?.());tr.remove();renumber();paintTools();changed();const next=rows[Math.min(at,rows.length-1)];(next?next.cells[0].instance.control||next.cells[0].instance.element:add).focus()});cell.appendChild(remove);tr.appendChild(cell);row.remove=remove;rows.push(row);body.appendChild(tr);renumber();paintTools();if(focus)(row.cells[0].instance.control||row.cells[0].instance.element).focus();return row};for(const values of Array.isArray(context.value)?context.value:[])addRow(values,false);add.type="button";setText(add,"+ "+labels.flowRecordAdd);add.setAttribute("aria-label",fill(labels.flowAddItem,{field:field.label}));add.hidden=context.readOnly;add.addEventListener("click",()=>{addRow(null,true);changed()});const empty=h("p","cx-flow-records-empty");setText(empty,labels.flowRecordsEmpty);const paintEmpty=()=>{empty.hidden=rows.length>0;table.hidden=!rows.length};group.addEventListener("change",paintEmpty);paintEmpty();const scroller=h("div","cx-flow-records-scroll");scroller.appendChild(table);group.append(scroller,empty,add);paintTools();return{element:group,control:group,group:true,focus:()=>(rows[0]?rows[0].cells[0].instance.control||rows[0].cells[0].instance.element:add).focus(),error:()=>{for(const[index,row]of rows.entries())for(const{column,instance}of row.cells){const problem=instance.error?.();if(problem)return fill(labels.flowRecordProblem,{row:index+1,message:problem})}
9
+ return null},getValue:()=>rows.map(row=>Object.fromEntries(row.cells.map(({column,instance})=>{const value=instance.getValue();return[column.id,value===""||value===undefined?null:value]}))),destroy:()=>rows.forEach(row=>{row.abort.abort();row.cells.forEach(({instance})=>instance.destroy?.())})}}
10
+ function flowRecordsProblem(view,field,rows){const labels=view.labels,title=field.label,items=field.items;if(!rows.length)return field.required?field.spec.rules.message||fill(labels.gridEditRequired,{column:title}):null;if(items.min!==undefined&&rows.length<items.min)return fill(labels.flowItemsMin,{field:title,min:items.min});if(items.max!==undefined&&rows.length>items.max)return fill(labels.flowItemsMax,{field:title,max:items.max});for(const[index,row]of rows.entries())for(const column of field.columns){const value=row[column.id]??null,shown=bound=>typeof bound==="number"?numberFormat(view.lang,0,20).format(bound):String(bound);const problem=column.type==="toggle"?null:formCheck(column.spec,value,column.type==="number"?"number":"text",column.title,labels,shown);if(problem)return fill(labels.flowRecordProblem,{row:index+1,message:problem})}
11
+ return null}
12
+ installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-records",part])})(typeof window!=="undefined"?window:globalThis);
@@ -0,0 +1,29 @@
1
+ /* The flow-tasks part: the hub of a task list. */
2
+ .cx-flow-tasks > .cx-flow-rail{display:none;}
3
+ .cx-flow-hub-progress{margin-top:var(--cx-size-4px);color:var(--cx-text-2);font-size:var(--cx-size-13d5px);}
4
+ .cx-flow-step > .cx-flow-task-groups{margin-top:var(--cx-flow-gap);}
5
+ .cx-flow-task-group + .cx-flow-task-group{margin-top:var(--cx-size-16px);}
6
+ .cx-flow-task-list{list-style:none;padding:0;border-top:var(--cx-size-1px) solid var(--cx-line);}
7
+ .cx-flow-task-group > .cx-flow-section-title{padding-bottom:var(--cx-size-6px);}
8
+ .cx-flow-task{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:var(--cx-size-2px) var(--cx-size-12px);padding:var(--cx-size-4px) 0;border-bottom:var(--cx-size-1px) solid var(--cx-line);}
9
+ .cx-flow-task-open{display:flex;flex-direction:column;align-items:flex-start;gap:var(--cx-size-2px);min-height:var(--cx-size-44px);padding:var(--cx-size-6px) var(--cx-size-8px);margin-inline-start:calc(var(--cx-size-8px) * -1);
10
+ border:0;border-radius:var(--cx-radius);background:transparent;color:var(--cx-accent-text);font:inherit;text-align:start;cursor:pointer;}
11
+ .cx-flow-task-open:hover{background:var(--cx-hover-bg);}
12
+ .cx-flow-task-open:focus-visible{outline:var(--cx-size-2px) solid var(--cx-accent);outline-offset:var(--cx-size-1px);}
13
+ .cx-flow-task-open[aria-disabled="true"]{color:var(--cx-text-2);cursor:default;}
14
+ .cx-flow-task-open[aria-disabled="true"]:hover{background:transparent;}
15
+ .cx-flow-task-title{font-weight:600;text-decoration:underline;text-underline-offset:var(--cx-size-2px);}
16
+ .cx-flow-task-open[aria-disabled="true"] .cx-flow-task-title{text-decoration:none;}
17
+ .cx-flow-task-summary{font-size:var(--cx-size-13px);color:var(--cx-text-2);}
18
+ .cx-flow-task-state{display:inline-flex;align-items:center;gap:var(--cx-size-6px);font-size:var(--cx-size-13px);color:var(--cx-muted);white-space:nowrap;}
19
+ .cx-flow-task-mark{display:inline-flex;align-items:center;justify-content:center;min-width:var(--cx-size-18px);height:var(--cx-size-18px);border-radius:50%;font-size:var(--cx-size-12px);font-weight:700;}
20
+ .cx-flow-task[data-cx-state="complete"] .cx-flow-task-mark{color:var(--cx-tone-success);background:var(--cx-tone-success-bg);}
21
+ .cx-flow-task[data-cx-state="complete"] .cx-flow-task-state{color:var(--cx-text-2);}
22
+ .cx-flow-task[data-cx-state="needsAction"] :is(.cx-flow-task-state,.cx-flow-task-mark){color:var(--cx-tone-warning);}
23
+ .cx-flow-task[data-cx-state="needsAction"] .cx-flow-task-mark{background:var(--cx-tone-warning-bg);}
24
+ .cx-flow-task[data-cx-state="error"] :is(.cx-flow-task-state,.cx-flow-task-mark){color:var(--cx-tone-danger);}
25
+ .cx-flow-task[data-cx-state="error"] .cx-flow-task-mark{background:var(--cx-tone-danger-bg);}
26
+ .cx-flow-task-blockers{grid-column:1 / -1;margin:0 0 var(--cx-size-4px);padding-inline-start:var(--cx-size-18px);font-size:var(--cx-size-13px);color:var(--cx-text-2);}
27
+ .cx-flow-hub-note{font-size:var(--cx-size-13px);color:var(--cx-muted);}
28
+ .cx-flow-next[aria-disabled="true"]{opacity:.55;cursor:default;}
29
+ @media (forced-colors:active){.cx-flow-task-mark{forced-color-adjust:none;border:var(--cx-size-1px) solid CanvasText;}}
@@ -0,0 +1,20 @@
1
+ (function(global){"use strict";const installed=new WeakSet();const part={version:"147dd31638a9cb94",requires:["flow","flow-host"],install(context){if(installed.has(context.FLOW_IMPL))return;const{FLOW_IMPL,FORMS_IMPL,h,setText,fill,plural,numberFormat}=context;const{FlowView,FLOW_HOST,FLOW_LABELS,flowFail,flowIs,flowKeys,flowName,flowString,flowFlag,flowScalar,flowFits,flowField,flowOptions,flowCondition,flowRefs,flowTest,flowCopy,flowSameValue,flowEmpty,flowNormal,flowDate,flowFieldView}=FLOW_IMPL.core;const{EDIT_EDITORS,formRuleSpec,formCheck}=FORMS_IMPL;const FLOW_PART_LABELS={flowTasks:"Tasks",flowTasksProgress:"{done} of {count} tasks complete",flowTaskNeedsAction:"Needs action",flowTaskBlocked:"Cannot start yet",flowTasksReview:"Review and submit",flowTasksIncomplete:"Complete the required tasks first.",flowBackToTasks:"Back to tasks",flowSaveAndReturn:"Save and return"};Object.assign(FLOW_LABELS,FLOW_PART_LABELS);const FLOW_TASK_STATES=["complete","needsAction","blocked","notStarted"];const FLOW_TASK_MARKS={complete:"✓",error:"!",needsAction:"!",blocked:"—",notStarted:""};function flowTaskOrder(parent,nodes){const wanted=new Set(nodes);for(const child of[...parent.children])if(!wanted.has(child))child.remove();let at=parent.firstChild;for(const node of nodes){if(node===at)at=at.nextSibling;else parent.insertBefore(node,at)}}
2
+ const{back:flowStepBack,goTo:flowStepGoTo,renderStage:flowStepStage,renderRail:flowStepRail,renderActions:flowStepActions,lastStep:flowLastStep,orientation:flowOrientation,begin:flowTaskBegin,setState:flowTaskRestore,submit:flowTaskSubmit,validateAll:flowTaskValidate}=FlowView.prototype;Object.assign(FlowView.prototype,{begin(values){this.hub??=this.model.mode==="tasks"?{id:"#tasks",type:"hub",title:null,asks:false,blocks:[],checks:[]}:null;this.hubNode=undefined;flowTaskBegin.call(this,values)},setState(state){const controller=this.statusController,result=flowTaskRestore.call(this,state);if(controller&&this.statusController===controller&&!this.destroyed&&this.step===this.hub)this.loadStatus();return result},orientation(){return this.hub?"tasks":flowOrientation.call(this)},lastStep(){return this.hub?this.step.type==="review":flowLastStep.call(this)},renderActions(){if(!this.hub||this.phase!=="form")return flowStepActions.call(this);if(this.step===this.hub){this.actions.hidden=false;if(this.hubAction?.go.parentNode!==this.actions){this.actions.replaceChildren();this.hubActions()}
3
+ this.paintHubActions();return}
4
+ flowStepActions.call(this);const back=this.actions.querySelector(".cx-flow-back");setText(back,this.labels.flowBackToTasks);back.hidden=false;if(!this.lastStep())setText(this.actions.querySelector(".cx-flow-next"),this.labels.flowSaveAndReturn)},taskButton(step){return step!==this.hub&&this.stage.querySelector('[data-cx-task="'+CSS.escape(step.id)+ '"] button')},async back(){return this.hub?!this.busy&&this.phase==="form"&&this.step!==this.hub&&this.go(this.hub,"back"):flowStepBack.call(this)},async goTo(id){if(!this.hub)return flowStepGoTo.call(this,id);if(this.busy||this.phase!=="form")return false;if(id===this.hub.id)return this.go(this.hub,"back");const target=this.visibleSteps().find(step=>step.id===id);return target&&this.taskOpens(target)?this.go(target,"forward"):false},renderStage(){if(this.hub&&this.step===this.hub&&this.phase==="form"){if(this.stage.firstChild!==this.hubNode)this.stage.replaceChildren(this.hubSection());else this.paintHub()}else flowStepStage.call(this)},renderRail(){if(!this.hub)return flowStepRail.call(this);if(!this.root)return;this.railKey=this.visibleSteps().map(step=>step.id).join();if(this.step===this.hub&&this.phase==="form")this.paintHub()},tasks(){return this.visibleSteps().filter(step=>step.type!=="review")},counted(step){return step.asks&&!step.optional},taskState(step){const host=this.stepStatus.get(step.id);if(host?.state==="blocked")return "blocked";if(this.stepErrorCount(step))return "error";if(host?.state==="needsAction")return "needsAction";if(host?.state==="complete"||this.completed.has(step.id))return "complete";return "notStarted"},taskOpens(step){return step.type==="review"?this.tasksReady():this.taskState(step)!=="blocked"},tasksReady(){return this.tasks().filter(step=>this.counted(step)).every(step=>this.taskState(step)==="complete")},async submit(){if(this.hub&&!this.tasksReady()){this.announce(this.labels.flowTasksIncomplete);return null}
5
+ return flowTaskSubmit.call(this)},async validateAll(...args){const result=await flowTaskValidate.apply(this,args);if(this.hub&&!this.tasksReady()){result.valid=false;result.errors.push({message:this.labels.flowTasksIncomplete})}
6
+ return result},hubAnnouncement(){const counted=this.tasks().filter(step=>this.counted(step));return fill(this.labels.flowTasksProgress,{done:counted.filter(step=>this.taskState(step)==="complete").length,count:counted.length})},hubSection(){const section=h("section","cx-flow-step cx-flow-hub"),heading=h("h3","cx-flow-step-title"),progress=h("p","cx-flow-hub-progress");heading.id=this.id+"-hub";heading.tabIndex=-1;setText(heading,this.labels.flowTasks);section.setAttribute("aria-labelledby",heading.id);progress.id=this.id+"-hub-progress";const lists=h("div","cx-flow-task-groups");section.append(heading,progress,lists);this.hubNode=section;this.paintHub();this.loadStatus();return section},paintHub(){const section=this.hubNode;if(!section)return;const labels=this.labels,lists=section.querySelector(".cx-flow-task-groups"),groups=new Map();for(const step of this.tasks()){const name=step.group||"";if(!groups.has(name))groups.set(name,[]);groups.get(name).push(step)}
7
+ const existing=new Map([...lists.children].map(group=>[group._cxName,group]));const rows=new Map([...lists.querySelectorAll("[data-cx-task]")].map(row=>[row.dataset.cxTask,row]));flowTaskOrder(lists,[...groups].map(([name,steps])=>{let group=existing.get(name);if(!group){group=h("section","cx-flow-task-group");group._cxName=name;if(name){const title=h("h4","cx-flow-section-title");title.id=this.id+"-tg-"+steps[0].id;setText(title,name);group.setAttribute("aria-labelledby",title.id);group.appendChild(title)}
8
+ group.appendChild(h("ol","cx-flow-task-list"))}
9
+ flowTaskOrder(group.querySelector("ol"),steps.map(step=>this.taskRow(step,rows.get(step.id))));return group}));setText(section.querySelector(".cx-flow-hub-progress"),this.hubAnnouncement());const banner=this.reviewStep()?null:this.banner;if(section._cxBanner!==banner){section.querySelector(":scope > .cx-flow-answer-banner")?.remove();if(banner)section.appendChild(this.answerBanner());section._cxBanner=banner}
10
+ if(this.step===this.hub)this.renderActions()},taskRow(step,row){const labels=this.labels,state=this.taskState(step),host=this.stepStatus.get(step.id);if(!row){row=h("li","cx-flow-task");const open=h("button","cx-flow-task-open"),title=h("span","cx-flow-task-title"),status=h("span","cx-flow-task-state"),mark=h("span","cx-flow-task-mark");const words=document.createTextNode(""),summary=h("span","cx-flow-task-summary"),blockers=h("ul","cx-flow-task-blockers");open.type="button";mark.setAttribute("aria-hidden","true");status.id=this.id+"-ts-"+step.id;summary.id=this.id+"-tsum-"+step.id;status.append(mark,words);open.append(title);row.append(open,status);open.addEventListener("click",()=>{if(open.getAttribute("aria-disabled")!=="true")this.goTo(step.id).catch(error=>this.report(error,"goTo"))});row._cx={open,title,status,mark,words,summary,blockers}}
11
+ const{open,title,status,mark,words,summary,blockers}=row._cx;row.dataset.cxTask=step.id;row.dataset.cxState=state;setText(title,step.title);words.nodeValue=state==="complete"?labels.flowStepComplete:state==="error"?fill(labels.flowStepErrors,{count:this.stepErrorCount(step)}):
12
+ state==="needsAction"?labels.flowTaskNeedsAction:state==="blocked"?labels.flowTaskBlocked:step.optional?labels.flowStepOptional:labels.flowStepNotStarted;setText(mark,FLOW_TASK_MARKS[state]||"");const notes=[status.id];if(host?.summary){setText(summary,host.summary);if(summary.parentNode!==open)open.appendChild(summary);notes.push(summary.id)}
13
+ else summary.remove();open.setAttribute("aria-describedby",notes.join(" "));if(state==="blocked")open.setAttribute("aria-disabled","true");else open.removeAttribute("aria-disabled");blockers.replaceChildren(...(host?.blockers||[]).map(text=>{const item=h("li");setText(item,text);return item}));if(blockers.children.length){if(blockers.parentNode!==row)row.appendChild(blockers)}else blockers.remove();return row},hubActions(){const labels=this.labels,review=this.reviewStep(),go=h("button","cx-flow-btn cx-primary cx-flow-next"),note=h("span","cx-flow-hub-note");go.type="button";setText(go,review?labels.flowTasksReview:this.model.submitLabel||labels.flowSubmit);note.id=this.id+"-hub-note";go.addEventListener("click",()=>{if(!this.tasksReady()){this.announce(labels.flowTasksIncomplete);return}
14
+ (review?this.goTo(review.id):this.submit()).catch(error=>this.report(error,review?"goTo":"submit"))});this.actions.append(...[this.draftButton?.(),this.cancelButton?.()].filter(Boolean),note,h("span","cx-flow-spacer"),go);this.hubAction={go,note}},paintHubActions(){const{go,note}=this.hubAction,ready=this.tasksReady();setText(note,ready?"":this.labels.flowTasksIncomplete);note.hidden=ready;if(!ready){go.setAttribute("aria-disabled","true");go.setAttribute("aria-describedby",note.id)}
15
+ else{go.removeAttribute("aria-disabled");go.removeAttribute("aria-describedby")}
16
+ go.disabled=!!this.busy;if(this.busy)go.setAttribute("aria-busy","true");else go.removeAttribute("aria-busy")},loadStatus(){if(typeof this.hooks.status!=="function")return;this.statusController?.abort();const controller=this.statusController=new AbortController();this.timed("status",signal=>this.hooks.status(this.getValues(),{signal}),controller).then(answer=>{if(this.statusController!==controller||this.destroyed||answer.cancelled||answer.failed)return;if(!flowIs(answer.value)){this.report(new Error("CanopyX: the status hook answers an object of step states."),"status");return}
17
+ for(const[id,state]of Object.entries(answer.value))if(this.model.steps.some(step=>step.id===id)){try{this.applyStepState(id,state)}catch(error){this.report(error,"status")}}
18
+ if(!this.tasksReady())this.retireOperation();this.paintHub()})},setStepState(id,state){if(!this.hub)flowFail("this flow does not use setStepState");if(!this.model.steps.some(step=>step.id===id))flowFail("there is no step '"+id+"'");this.applyStepState(id,state);if(!this.tasksReady())this.retireOperation();if(this.step===this.hub)this.paintHub();else this.renderRail()},applyStepState(id,state){if(state===null){this.stepStatus.delete(id);return}
19
+ const text=value=>typeof value==="string"&&/\S/.test(value) ? value.slice(0, 400) : null;
20
+ if(!flowIs(state)||!FLOW_TASK_STATES.includes(state.state)||state.blockers!==undefined&&!Array.isArray(state.blockers))flowFail("a step state is { state: complete | needsAction | blocked | notStarted, summary, blockers }");this.stepStatus.set(id,{state:state.state,summary:text(state.summary),blockers:(state.blockers||[]).map(text).filter(Boolean).slice(0,10)})}});installed.add(context.FLOW_IMPL)}};(global.CanopyXParts||=[]).push(["flow-tasks",part])})(typeof window!=="undefined"?window:globalThis);