cardframing

This commit is contained in:
legop3
2026-09-14 12:41:25 -04:00
parent 3c385126ae
commit ec8eb1c002
14 changed files with 113 additions and 91 deletions
+2 -1
View File
@@ -443,7 +443,8 @@ Implemented on 2026-09-14:
- Added the centralized `/admin` route with Overview, Fleet operations, Users and administrators, and one schema-generated hierarchical Configuration page in legacy YAML order. - Added the centralized `/admin` route with Overview, Fleet operations, Users and administrators, and one schema-generated hierarchical Configuration page in legacy YAML order.
- Replaced every feature-specific configuration form with `@rjsf/core`; the protected admin snapshot supplies the server's assembled schema, and one generic widget handles all schema-declared secrets. - Replaced every feature-specific configuration form with `@rjsf/core`; the protected admin snapshot supplies the server's assembled schema, and one generic widget handles all schema-declared secrets.
- Replaced RJSF's unthemed Bootstrap markup with a generic MultiRover tree renderer. The complete document now follows schema order as indented object, array, item, and key/value rows; array controls remain readable text beside each item, and the route-specific styling lives outside the global stylesheet. - Replaced RJSF's unthemed Bootstrap markup with a generic MultiRover tree renderer. The complete document now follows schema order as indented object, array, item, and key/value rows; array controls remain readable text beside each item, and the route-specific styling lives outside the global stylesheet.
- Increased only the top-level section spacing while keeping nested configuration lines compact, moved option descriptions into the wider value column, and made root and array-item descriptions visible instead of discarding them. - Replaced the editor's custom section borders, header backgrounds, and indentation guides with the application's shared `CardFrame` at every object, array, and array-item layer. Scalar settings remain compact key/value rows, descriptions use the wider value column, and collection actions stay beside their content instead of moving to the far edge.
- Disabled RJSF's internal checkbox label and description generically, leaving the shared field row as the single owner of each boolean setting's name, required marker, and description.
- Traced all 156 schema nodes to their runtime consumers and added operator-facing descriptions for every root, section, collection, array item, and scalar option. A recursive configuration test now rejects any future schema node without a description; currently reserved settings explicitly state that they have no runtime effect. - Traced all 156 schema nodes to their runtime consumers and added operator-facing descriptions for every root, section, collection, array item, and scalar option. A recursive configuration test now rejects any future schema node without a description; currently reserved settings explicitly state that they have no runtime effect.
- Converged feature control into service-owned configuration: each public feature opts in beside its own schema, and the configuration system derives those exact `enabled` switches for sessions and command discovery. The former server feature registry was removed; configuration completeness and hardware availability remain visible as runtime status instead of becoming hidden enablement rules. - Converged feature control into service-owned configuration: each public feature opts in beside its own schema, and the configuration system derives those exact `enabled` switches for sessions and command discovery. The former server feature registry was removed; configuration completeness and hardware availability remain visible as runtime status instead of becoming hidden enablement rules.
- Lazy-loaded setup and administration so the schema-form dependency is not included in ordinary driver-page downloads. - Lazy-loaded setup and administration so the schema-form dependency is not included in ordinary driver-page downloads.
@@ -1 +0,0 @@
.configuration-tree{margin-top:.25rem}.configuration-tree>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.configuration-branch{min-width:0px}.configuration-branch-heading,.configuration-item-heading{display:flex;flex-wrap:wrap;align-items:center;gap:.125rem;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity));padding:.125rem}.configuration-branch-heading h3,.configuration-item-title{font-size:.875rem;line-height:1.25rem;font-weight:600;--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}.configuration-children{margin-left:.25rem}.configuration-children>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.configuration-children{border-left-width:1px;border-color:#0369a1b3;padding-top:.125rem;padding-bottom:.125rem;padding-left:.25rem}.configuration-line{display:grid;min-width:0px;grid-template-columns:repeat(1,minmax(0,1fr));align-items:flex-start;gap:.125rem;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity));padding:.125rem}@media(min-width:640px){.configuration-line{grid-template-columns:minmax(9rem,16rem) minmax(12rem,40rem)}}.configuration-line{justify-content:start}.configuration-key,.configuration-value{min-width:0px}.configuration-root-description,.configuration-branch-description,.configuration-item-description,.configuration-value-description{display:block;font-size:.7rem;line-height:1.25;--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity));margin-top:.125rem}.configuration-root-description{margin-bottom:.25rem}.configuration-branch-description,.configuration-item-description{margin-left:.125rem;max-width:56rem}.configuration-value-description{margin-bottom:.125rem;max-width:40rem}.configuration-value input:not([type=checkbox]),.configuration-value select,.configuration-value textarea{width:100%;border-radius:.375rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity));padding:.125rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.configuration-value input:not([type=checkbox])::-moz-placeholder,.configuration-value select::-moz-placeholder,.configuration-value textarea::-moz-placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.configuration-value input:not([type=checkbox])::placeholder,.configuration-value select::placeholder,.configuration-value textarea::placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.configuration-value input:not([type=checkbox]):focus,.configuration-value select:focus,.configuration-value textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-opacity: 1;--tw-ring-color: rgb(14 165 233 / var(--tw-ring-opacity))}.configuration-value input[type=checkbox]{height:1rem;width:1rem;vertical-align:middle;accent-color:#0ea5e9}.configuration-value .checkbox label{display:flex;min-height:1.75rem;align-items:center;--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}.configuration-value .error-detail{margin-top:.125rem;font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity))}.configuration-value .help-block{margin-top:.125rem;display:block;font-size:.7rem;--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}.configuration-secret{min-width:0px}.configuration-array-item+.configuration-array-item{margin-top:.125rem}
@@ -0,0 +1 @@
.configuration-tree{margin-top:.25rem}.configuration-tree>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.configuration-card{min-width:0px}.configuration-card-body>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.configuration-card-body{padding:.125rem}.configuration-children>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.configuration-line{display:grid;min-width:0px;grid-template-columns:repeat(1,minmax(0,1fr));align-items:flex-start;gap:.125rem;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity));padding:.125rem}@media(min-width:640px){.configuration-line{grid-template-columns:minmax(9rem,16rem) minmax(12rem,40rem)}}.configuration-line{justify-content:start}.configuration-key,.configuration-value{min-width:0px}.configuration-root-description,.configuration-branch-description,.configuration-item-description,.configuration-value-description{display:block;font-size:.7rem;line-height:1.25;--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity));margin-top:.125rem}.configuration-root-description{margin-bottom:.25rem}.configuration-branch-description,.configuration-item-description{max-width:56rem}.configuration-value-description{margin-bottom:.125rem;max-width:40rem}.configuration-value input:not([type=checkbox]),.configuration-value select,.configuration-value textarea{width:100%;border-radius:.375rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity));padding:.125rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.configuration-value input:not([type=checkbox])::-moz-placeholder,.configuration-value select::-moz-placeholder,.configuration-value textarea::-moz-placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.configuration-value input:not([type=checkbox])::placeholder,.configuration-value select::placeholder,.configuration-value textarea::placeholder{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}.configuration-value input:not([type=checkbox]):focus,.configuration-value select:focus,.configuration-value textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-opacity: 1;--tw-ring-color: rgb(14 165 233 / var(--tw-ring-opacity))}.configuration-value input[type=checkbox]{height:1rem;width:1rem;vertical-align:middle;accent-color:#0ea5e9}.configuration-value .checkbox label{display:flex;min-height:1.75rem;align-items:center;--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity))}.configuration-value .error-detail{margin-top:.125rem;font-size:.75rem;line-height:1rem;--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity))}.configuration-value .help-block{margin-top:.125rem;display:block;font-size:.7rem;--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity))}.configuration-secret{min-width:0px}.configuration-item-actions,.configuration-array-actions{display:flex;flex-wrap:wrap;gap:.125rem}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
import{e as F,r as s,j as e,S as P,C as c,L as $}from"./index-_sqiQMZb.js";import{e as q,f as D,i as I}from"./api-C0PIt_OP.js";function R(){const l=F(),[a,m]=s.useState(null),[n,b]=s.useState(""),[d,C]=s.useState(""),[p,N]=s.useState(""),[r,S]=s.useState(""),[f,v]=s.useState(""),[o,w]=s.useState(null),[x,h]=s.useState(!1),[g,i]=s.useState("");s.useEffect(()=>{q(l).then(t=>m(t.required)).catch(t=>i(t.message))},[l]);async function j(t){h(!0),i("");try{await t(),m(!1),i("Setup completed. You can now open the administration application and log in.")}catch(u){const E=Array.isArray(u.validationErrors)?` ${u.validationErrors.map(y=>`${y.path}: ${y.message}`).join("; ")}`:"";i(`${u.message}${E}`)}finally{h(!1)}}function k(t){if(t.preventDefault(),r!==f){i("Passwords do not match.");return}j(()=>D(l,{setupCode:n,username:d,discordId:p,password:r}))}function A(t){t.preventDefault(),o&&j(async()=>I(l,{setupCode:n,fileName:o.name,yaml:await o.text()}))}return e.jsxs("div",{className:"min-h-screen bg-neutral-950 p-1 text-slate-100",children:[e.jsx(P,{}),e.jsxs("main",{className:"mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center gap-0.5",children:[e.jsxs(c,{title:"MultiRover setup",meta:a===null?"checking":a?"required":"complete",bodyClassName:"space-y-0.5 p-1 text-sm",children:[a?e.jsx("p",{children:"Enter the one-time code from setup-code.txt in the server data folder, then create the first lockdown administrator or import an existing configuration."}):null,a===!1?e.jsx($,{className:"button-dark inline-block",to:"/admin",children:"Open administration"}):null,g?e.jsx("p",{className:"surface p-1 text-sm text-slate-200",children:g}):null]}),a?e.jsxs(e.Fragment,{children:[e.jsxs(c,{title:"Setup authorization",bodyClassName:"p-1",children:[e.jsx("label",{className:"block text-xs font-semibold text-slate-200",children:"One-time setup code"}),e.jsx("input",{className:"field-input mt-0.5 w-full font-mono",value:n,onChange:t=>b(t.target.value)})]}),e.jsx(c,{title:"Create first administrator",bodyClassName:"p-1",children:e.jsxs("form",{className:"grid gap-0.5 md:grid-cols-2",onSubmit:k,children:[e.jsx("input",{className:"field-input",placeholder:"Username",value:d,onChange:t=>C(t.target.value)}),e.jsx("input",{className:"field-input",placeholder:"Discord id (optional)",value:p,onChange:t=>N(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Password",value:r,onChange:t=>S(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Confirm password",value:f,onChange:t=>v(t.target.value)}),e.jsx("button",{className:"button-dark md:col-span-2",type:"submit",disabled:x||!n||!d||!r,children:"Create lockdown administrator"})]})}),e.jsxs(c,{title:"Import configuration file",bodyClassName:"space-y-0.5 p-1 text-sm",children:[e.jsx("p",{className:"text-xs text-slate-400",children:"Choose an existing YAML configuration explicitly. The server validates and imports it once, and its secrets are never displayed back in the browser."}),e.jsxs("form",{className:"flex flex-col gap-0.5 md:flex-row",onSubmit:A,children:[e.jsx("input",{className:"field-input flex-1",type:"file",accept:".yaml,.yml,text/yaml",onChange:t=>w(t.target.files?.[0]||null)}),e.jsx("button",{className:"button-dark",type:"submit",disabled:x||!n||!o,children:"Import selected YAML"})]})]})]}):null]})]})}export{R as default}; import{e as F,r as s,j as e,S as P,C as c,L as $}from"./index-CeCKuewK.js";import{e as q,f as D,i as I}from"./api-C0PIt_OP.js";function R(){const l=F(),[a,m]=s.useState(null),[n,b]=s.useState(""),[d,C]=s.useState(""),[p,N]=s.useState(""),[r,S]=s.useState(""),[f,v]=s.useState(""),[o,w]=s.useState(null),[x,h]=s.useState(!1),[g,i]=s.useState("");s.useEffect(()=>{q(l).then(t=>m(t.required)).catch(t=>i(t.message))},[l]);async function j(t){h(!0),i("");try{await t(),m(!1),i("Setup completed. You can now open the administration application and log in.")}catch(u){const E=Array.isArray(u.validationErrors)?` ${u.validationErrors.map(y=>`${y.path}: ${y.message}`).join("; ")}`:"";i(`${u.message}${E}`)}finally{h(!1)}}function k(t){if(t.preventDefault(),r!==f){i("Passwords do not match.");return}j(()=>D(l,{setupCode:n,username:d,discordId:p,password:r}))}function A(t){t.preventDefault(),o&&j(async()=>I(l,{setupCode:n,fileName:o.name,yaml:await o.text()}))}return e.jsxs("div",{className:"min-h-screen bg-neutral-950 p-1 text-slate-100",children:[e.jsx(P,{}),e.jsxs("main",{className:"mx-auto flex min-h-screen w-full max-w-3xl flex-col justify-center gap-0.5",children:[e.jsxs(c,{title:"MultiRover setup",meta:a===null?"checking":a?"required":"complete",bodyClassName:"space-y-0.5 p-1 text-sm",children:[a?e.jsx("p",{children:"Enter the one-time code from setup-code.txt in the server data folder, then create the first lockdown administrator or import an existing configuration."}):null,a===!1?e.jsx($,{className:"button-dark inline-block",to:"/admin",children:"Open administration"}):null,g?e.jsx("p",{className:"surface p-1 text-sm text-slate-200",children:g}):null]}),a?e.jsxs(e.Fragment,{children:[e.jsxs(c,{title:"Setup authorization",bodyClassName:"p-1",children:[e.jsx("label",{className:"block text-xs font-semibold text-slate-200",children:"One-time setup code"}),e.jsx("input",{className:"field-input mt-0.5 w-full font-mono",value:n,onChange:t=>b(t.target.value)})]}),e.jsx(c,{title:"Create first administrator",bodyClassName:"p-1",children:e.jsxs("form",{className:"grid gap-0.5 md:grid-cols-2",onSubmit:k,children:[e.jsx("input",{className:"field-input",placeholder:"Username",value:d,onChange:t=>C(t.target.value)}),e.jsx("input",{className:"field-input",placeholder:"Discord id (optional)",value:p,onChange:t=>N(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Password",value:r,onChange:t=>S(t.target.value)}),e.jsx("input",{className:"field-input",type:"password",placeholder:"Confirm password",value:f,onChange:t=>v(t.target.value)}),e.jsx("button",{className:"button-dark md:col-span-2",type:"submit",disabled:x||!n||!d||!r,children:"Create lockdown administrator"})]})}),e.jsxs(c,{title:"Import configuration file",bodyClassName:"space-y-0.5 p-1 text-sm",children:[e.jsx("p",{className:"text-xs text-slate-400",children:"Choose an existing YAML configuration explicitly. The server validates and imports it once, and its secrets are never displayed back in the browser."}),e.jsxs("form",{className:"flex flex-col gap-0.5 md:flex-row",onSubmit:A,children:[e.jsx("input",{className:"field-input flex-1",type:"file",accept:".yaml,.yml,text/yaml",onChange:t=>w(t.target.files?.[0]||null)}),e.jsx("button",{className:"button-dark",type:"submit",disabled:x||!n||!o,children:"Import selected YAML"})]})]})]}):null]})]})}export{R as default};
//# sourceMappingURL=SetupApp-CkP7exMm.js.map //# sourceMappingURL=SetupApp-DT0amLuj.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -12,7 +12,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<!-- site-metadata:inject --> <!-- site-metadata:inject -->
<!-- analytics:inject --> <!-- analytics:inject -->
<script type="module" crossorigin src="/assets/index-_sqiQMZb.js"></script> <script type="module" crossorigin src="/assets/index-CeCKuewK.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-1O6avznD.css"> <link rel="stylesheet" crossorigin href="/assets/index-1O6avznD.css">
</head> </head>
<body> <body>
@@ -4,6 +4,7 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import Form from '@rjsf/core'; import Form from '@rjsf/core';
import validator from '@rjsf/validator-ajv8'; import validator from '@rjsf/validator-ajv8';
import CardFrame from '../../components/CardFrame/index.jsx';
function buildUiSchema(schema, path = '') { function buildUiSchema(schema, path = '') {
/* /*
@@ -19,6 +20,20 @@ function buildUiSchema(schema, path = '') {
}; };
} }
if (schema.type === 'boolean') {
/*
RJSF's checkbox is unusual: unless it is explicitly selected, it renders
the schema description and field name inside the widget as well as
passing them to FieldTemplate. This generic option makes the surrounding
YAML-like row the sole owner of that text, eliminating duplicates without
maintaining a list of boolean setting names.
*/
return {
'ui:widget': 'checkbox',
'ui:options': { label: false },
};
}
if (schema.type !== 'object' || !schema.properties) return {}; if (schema.type !== 'object' || !schema.properties) return {};
return Object.fromEntries(Object.entries(schema.properties).map(([key, childSchema]) => [ return Object.fromEntries(Object.entries(schema.properties).map(([key, childSchema]) => [
key, key,
@@ -136,8 +151,8 @@ function ConfigurationObjectTemplate({ description, fieldPathId, properties, tit
if (typeof fieldPathId.path.at(-1) === 'number') { if (typeof fieldPathId.path.at(-1) === 'number') {
// Array items receive their numbered heading and action row from the array // Array items receive their numbered heading and action row from the array
// item template. Rendering only their description and ordered property // item CardFrame. Rendering only their description and ordered properties
// lines prevents redundant boxes while preserving the item's own guidance. // prevents a redundant anonymous card inside that visible item boundary.
return ( return (
<div> <div>
{description ? <div className="configuration-item-description">{description}</div> : null} {description ? <div className="configuration-item-description">{description}</div> : null}
@@ -147,13 +162,15 @@ function ConfigurationObjectTemplate({ description, fieldPathId, properties, tit
} }
return ( return (
<section className="configuration-branch"> <CardFrame
<header className="configuration-branch-heading"> title={title}
<h3>{title}</h3> clipOverflow={false}
</header> className="configuration-card"
bodyClassName="configuration-card-body"
>
{description ? <div className="configuration-branch-description">{description}</div> : null} {description ? <div className="configuration-branch-description">{description}</div> : null}
<div className="configuration-children">{propertyLines}</div> <div className="configuration-children">{propertyLines}</div>
</section> </CardFrame>
); );
} }
@@ -161,13 +178,17 @@ function ConfigurationArrayItemTemplate({ buttonsProps, children, hasToolbar, in
const unavailable = buttonsProps.disabled || buttonsProps.readonly; const unavailable = buttonsProps.disabled || buttonsProps.readonly;
return ( return (
<article className="configuration-branch configuration-array-item"> <CardFrame
title={`Item ${index + 1}`}
clipOverflow={false}
className="configuration-card configuration-array-item"
bodyClassName="configuration-card-body"
>
{hasToolbar ? ( {hasToolbar ? (
// Text controls are intentionally kept immediately after the item // Actions remain at the beginning of the card body instead of using
// number. RJSF's default Bootstrap toolbox pushes empty glyphicon // CardFrame's right-aligned action slot. Even on a wide editor, item
// buttons to the far edge, which is both unclear and hard to reach. // controls therefore stay next to the content they affect.
<header className="configuration-item-heading"> <div className="configuration-item-actions">
<span className="configuration-item-title">Item {index + 1}</span>
{(buttonsProps.hasMoveUp || buttonsProps.hasMoveDown) ? ( {(buttonsProps.hasMoveUp || buttonsProps.hasMoveDown) ? (
<button type="button" className="button-dark text-xs" disabled={unavailable || !buttonsProps.hasMoveUp} onClick={buttonsProps.onMoveUpItem}>Move up</button> <button type="button" className="button-dark text-xs" disabled={unavailable || !buttonsProps.hasMoveUp} onClick={buttonsProps.onMoveUpItem}>Move up</button>
) : null} ) : null}
@@ -180,28 +201,34 @@ function ConfigurationArrayItemTemplate({ buttonsProps, children, hasToolbar, in
{buttonsProps.hasRemove ? ( {buttonsProps.hasRemove ? (
<button type="button" className="button-danger text-xs" disabled={unavailable} onClick={buttonsProps.onRemoveItem}>Remove</button> <button type="button" className="button-danger text-xs" disabled={unavailable} onClick={buttonsProps.onRemoveItem}>Remove</button>
) : null} ) : null}
</header> </div>
) : null} ) : null}
<div className="configuration-children">{children}</div> <div className="configuration-children">{children}</div>
</article> </CardFrame>
); );
} }
function ConfigurationArrayTemplate({ canAdd, disabled, items, onAddClick, readonly, schema, title }) { function ConfigurationArrayTemplate({ canAdd, disabled, items, onAddClick, readonly, schema, title }) {
return ( return (
<section className="configuration-branch configuration-array"> <CardFrame
<header className="configuration-branch-heading configuration-array-heading"> title={title}
<h3>{title}</h3> meta={`${items.length} ${items.length === 1 ? 'item' : 'items'}`}
<span className="text-[0.7rem] text-slate-400">{items.length} {items.length === 1 ? 'item' : 'items'}</span> clipOverflow={false}
</header> className="configuration-card configuration-array"
bodyClassName="configuration-card-body"
>
{schema.description ? <div className="configuration-branch-description">{schema.description}</div> : null} {schema.description ? <div className="configuration-branch-description">{schema.description}</div> : null}
{canAdd ? (
// Adding belongs to the collection as a whole, but stays left-aligned
// with that collection's contents rather than at the viewport edge.
<div className="configuration-array-actions">
<button type="button" className="button-dark text-xs" disabled={disabled || readonly} onClick={onAddClick}>Add item</button>
</div>
) : null}
<div className="configuration-children"> <div className="configuration-children">
{items.length ? <div className="space-y-0.5">{items}</div> : <p className="text-xs text-slate-500">No items configured.</p>} {items.length ? items : <p className="text-xs text-slate-500">No items configured.</p>}
{canAdd ? (
<button type="button" className="button-dark mt-0.5 text-xs" disabled={disabled || readonly} onClick={onAddClick}>Add item</button>
) : null}
</div> </div>
</section> </CardFrame>
); );
} }
+18 -24
View File
@@ -4,33 +4,24 @@
*/ */
.configuration-tree { .configuration-tree {
/* A single ordered stack is the visual equivalent of YAML's top-to-bottom /* CardFrame now provides every structural boundary. This rule only preserves
document order. A deliberately large root gap makes the beginning and end the ordered document flow and a normal gap between sibling root cards. */
of every top-level section unmistakable, while nested lines stay compact. */ @apply mt-1 space-y-1;
@apply mt-1 space-y-4;
} }
.configuration-branch { .configuration-card {
@apply min-w-0; @apply min-w-0;
} }
.configuration-branch-heading, .configuration-card-body {
.configuration-item-heading { /* The shared card supplies the border, header, and background. Its body needs
/* Headings and their controls use normal left-to-right flow. Deliberately only compact padding and vertical rhythm for descriptions, actions, rows,
omitting space-between prevents wide pages from marooning actions. */ and any nested CardFrames. */
@apply flex flex-wrap items-center gap-0.5 rounded-md bg-slate-800 px-0.5 py-0.5; @apply space-y-0.5 p-0.5;
}
.configuration-branch-heading h3,
.configuration-item-title {
@apply text-sm font-semibold text-slate-100;
} }
.configuration-children { .configuration-children {
/* Every container contributes exactly one indentation and one guide. Deep @apply space-y-0.5;
descendants therefore explain their ancestry without calculating depth or
encoding service paths in frontend code. */
@apply ml-1 space-y-0.5 border-l border-sky-700/70 py-0.5 pl-1;
} }
.configuration-line { .configuration-line {
@@ -59,9 +50,9 @@
.configuration-branch-description, .configuration-branch-description,
.configuration-item-description { .configuration-item-description {
/* Container help follows its heading at the same indentation; child keys /* Structural descriptions sit inside the corresponding CardFrame body, so
begin below it, preserving the same parent/child rhythm as YAML comments. */ no custom border, header background, or indentation treatment is needed. */
@apply ml-0.5 max-w-[56rem]; @apply max-w-[56rem];
} }
.configuration-value-description { .configuration-value-description {
@@ -98,6 +89,9 @@
@apply min-w-0; @apply min-w-0;
} }
.configuration-array-item + .configuration-array-item { .configuration-item-actions,
@apply mt-0.5; .configuration-array-actions {
/* Actions use ordinary flow intentionally. CardFrame's action slot is useful
elsewhere, but would separate these controls from wide configuration data. */
@apply flex flex-wrap gap-0.5;
} }