mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
cardframing
This commit is contained in:
@@ -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.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -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}
|
||||
+28
-28
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
@@ -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};
|
||||
//# sourceMappingURL=SetupApp-CkP7exMm.js.map
|
||||
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-DT0amLuj.js.map
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -12,7 +12,7 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<!-- site-metadata: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">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import Form from '@rjsf/core';
|
||||
import validator from '@rjsf/validator-ajv8';
|
||||
import CardFrame from '../../components/CardFrame/index.jsx';
|
||||
|
||||
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 {};
|
||||
return Object.fromEntries(Object.entries(schema.properties).map(([key, childSchema]) => [
|
||||
key,
|
||||
@@ -136,8 +151,8 @@ function ConfigurationObjectTemplate({ description, fieldPathId, properties, tit
|
||||
|
||||
if (typeof fieldPathId.path.at(-1) === 'number') {
|
||||
// Array items receive their numbered heading and action row from the array
|
||||
// item template. Rendering only their description and ordered property
|
||||
// lines prevents redundant boxes while preserving the item's own guidance.
|
||||
// item CardFrame. Rendering only their description and ordered properties
|
||||
// prevents a redundant anonymous card inside that visible item boundary.
|
||||
return (
|
||||
<div>
|
||||
{description ? <div className="configuration-item-description">{description}</div> : null}
|
||||
@@ -147,13 +162,15 @@ function ConfigurationObjectTemplate({ description, fieldPathId, properties, tit
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="configuration-branch">
|
||||
<header className="configuration-branch-heading">
|
||||
<h3>{title}</h3>
|
||||
</header>
|
||||
<CardFrame
|
||||
title={title}
|
||||
clipOverflow={false}
|
||||
className="configuration-card"
|
||||
bodyClassName="configuration-card-body"
|
||||
>
|
||||
{description ? <div className="configuration-branch-description">{description}</div> : null}
|
||||
<div className="configuration-children">{propertyLines}</div>
|
||||
</section>
|
||||
</CardFrame>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,13 +178,17 @@ function ConfigurationArrayItemTemplate({ buttonsProps, children, hasToolbar, in
|
||||
const unavailable = buttonsProps.disabled || buttonsProps.readonly;
|
||||
|
||||
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 ? (
|
||||
// Text controls are intentionally kept immediately after the item
|
||||
// number. RJSF's default Bootstrap toolbox pushes empty glyphicon
|
||||
// buttons to the far edge, which is both unclear and hard to reach.
|
||||
<header className="configuration-item-heading">
|
||||
<span className="configuration-item-title">Item {index + 1}</span>
|
||||
// Actions remain at the beginning of the card body instead of using
|
||||
// CardFrame's right-aligned action slot. Even on a wide editor, item
|
||||
// controls therefore stay next to the content they affect.
|
||||
<div className="configuration-item-actions">
|
||||
{(buttonsProps.hasMoveUp || buttonsProps.hasMoveDown) ? (
|
||||
<button type="button" className="button-dark text-xs" disabled={unavailable || !buttonsProps.hasMoveUp} onClick={buttonsProps.onMoveUpItem}>Move up</button>
|
||||
) : null}
|
||||
@@ -180,28 +201,34 @@ function ConfigurationArrayItemTemplate({ buttonsProps, children, hasToolbar, in
|
||||
{buttonsProps.hasRemove ? (
|
||||
<button type="button" className="button-danger text-xs" disabled={unavailable} onClick={buttonsProps.onRemoveItem}>Remove</button>
|
||||
) : null}
|
||||
</header>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="configuration-children">{children}</div>
|
||||
</article>
|
||||
</CardFrame>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfigurationArrayTemplate({ canAdd, disabled, items, onAddClick, readonly, schema, title }) {
|
||||
return (
|
||||
<section className="configuration-branch configuration-array">
|
||||
<header className="configuration-branch-heading configuration-array-heading">
|
||||
<h3>{title}</h3>
|
||||
<span className="text-[0.7rem] text-slate-400">{items.length} {items.length === 1 ? 'item' : 'items'}</span>
|
||||
</header>
|
||||
<CardFrame
|
||||
title={title}
|
||||
meta={`${items.length} ${items.length === 1 ? 'item' : 'items'}`}
|
||||
clipOverflow={false}
|
||||
className="configuration-card configuration-array"
|
||||
bodyClassName="configuration-card-body"
|
||||
>
|
||||
{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">
|
||||
{items.length ? <div className="space-y-0.5">{items}</div> : <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}
|
||||
{items.length ? items : <p className="text-xs text-slate-500">No items configured.</p>}
|
||||
</div>
|
||||
</section>
|
||||
</CardFrame>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+18
-24
@@ -4,33 +4,24 @@
|
||||
*/
|
||||
|
||||
.configuration-tree {
|
||||
/* A single ordered stack is the visual equivalent of YAML's top-to-bottom
|
||||
document order. A deliberately large root gap makes the beginning and end
|
||||
of every top-level section unmistakable, while nested lines stay compact. */
|
||||
@apply mt-1 space-y-4;
|
||||
/* CardFrame now provides every structural boundary. This rule only preserves
|
||||
the ordered document flow and a normal gap between sibling root cards. */
|
||||
@apply mt-1 space-y-1;
|
||||
}
|
||||
|
||||
.configuration-branch {
|
||||
.configuration-card {
|
||||
@apply min-w-0;
|
||||
}
|
||||
|
||||
.configuration-branch-heading,
|
||||
.configuration-item-heading {
|
||||
/* Headings and their controls use normal left-to-right flow. Deliberately
|
||||
omitting space-between prevents wide pages from marooning actions. */
|
||||
@apply flex flex-wrap items-center gap-0.5 rounded-md bg-slate-800 px-0.5 py-0.5;
|
||||
}
|
||||
|
||||
.configuration-branch-heading h3,
|
||||
.configuration-item-title {
|
||||
@apply text-sm font-semibold text-slate-100;
|
||||
.configuration-card-body {
|
||||
/* The shared card supplies the border, header, and background. Its body needs
|
||||
only compact padding and vertical rhythm for descriptions, actions, rows,
|
||||
and any nested CardFrames. */
|
||||
@apply space-y-0.5 p-0.5;
|
||||
}
|
||||
|
||||
.configuration-children {
|
||||
/* Every container contributes exactly one indentation and one guide. Deep
|
||||
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;
|
||||
@apply space-y-0.5;
|
||||
}
|
||||
|
||||
.configuration-line {
|
||||
@@ -59,9 +50,9 @@
|
||||
|
||||
.configuration-branch-description,
|
||||
.configuration-item-description {
|
||||
/* Container help follows its heading at the same indentation; child keys
|
||||
begin below it, preserving the same parent/child rhythm as YAML comments. */
|
||||
@apply ml-0.5 max-w-[56rem];
|
||||
/* Structural descriptions sit inside the corresponding CardFrame body, so
|
||||
no custom border, header background, or indentation treatment is needed. */
|
||||
@apply max-w-[56rem];
|
||||
}
|
||||
|
||||
.configuration-value-description {
|
||||
@@ -98,6 +89,9 @@
|
||||
@apply min-w-0;
|
||||
}
|
||||
|
||||
.configuration-array-item + .configuration-array-item {
|
||||
@apply mt-0.5;
|
||||
.configuration-item-actions,
|
||||
.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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user