mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
cardframing again
This commit is contained in:
@@ -446,7 +446,8 @@ Implemented on 2026-09-14:
|
||||
- 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.
|
||||
- Restored the former example YAML's installation-specific values as both schema-owned input examples and the actual initial values for non-secret settings and collection shapes. The only intentionally empty defaults are the three credentials and active driver HTML; their placeholders still explain the expected input without falsely marking credentials as configured or publishing sample content.
|
||||
- Strengthened top-level hierarchy with a 1.5-rem sibling gap, distinct dark-blue top-level `CardFrame` headers, and repeated indentation for each nested card's contents.
|
||||
- Strengthened top-level hierarchy with a 1.5-rem sibling gap while retaining compact spacing within each configuration section.
|
||||
- Extended `CardFrame` with an optional explicit accent while preserving its assigned-rover default, then gave every configuration nesting level its own complete header-and-border accent. Nested CardFrames themselves now carry the YAML-like indentation, scalar contents remain aligned with their owning card, and descriptions use a larger, higher-contrast treatment.
|
||||
- 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(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * 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{margin-left:.5rem}.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}
|
||||
@@ -0,0 +1 @@
|
||||
.configuration-tree{margin-top:.25rem}.configuration-tree>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.configuration-card{min-width:0px}.configuration-card .configuration-card{margin-left:1rem;width:calc(100% - 1rem)}.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:.875rem;line-height:1.25rem;line-height:1.375;--tw-text-opacity: 1;color:rgb(203 213 225 / 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}
|
||||
+45
-45
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-D5baJIet.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-BTQ-8jXK.js.map
|
||||
import{e as F,r as s,j as e,S as P,C as c,L as $}from"./index-CnHL-xtk.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-Ng2sxryp.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
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
@@ -12,8 +12,8 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<!-- site-metadata:inject -->
|
||||
<!-- analytics:inject -->
|
||||
<script type="module" crossorigin src="/assets/index-D5baJIet.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DlNVuQB1.css">
|
||||
<script type="module" crossorigin src="/assets/index-CnHL-xtk.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-1O6avznD.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,11 +1,37 @@
|
||||
// Schema-Generated Configuration Form
|
||||
// Purpose: Renders the server-provided JSON Schema as an ordered, indented tree resembling the configuration's YAML structure.
|
||||
// Scope: Defines one generic RJSF presentation; it never names or special-cases an individual service or setting.
|
||||
import { useMemo } from 'react';
|
||||
import { createContext, useContext, useMemo } from 'react';
|
||||
import Form from '@rjsf/core';
|
||||
import validator from '@rjsf/validator-ajv8';
|
||||
import CardFrame from '../../components/CardFrame/index.jsx';
|
||||
|
||||
/*
|
||||
Structural colors repeat only after six levels, which is deeper than the
|
||||
current configuration document but remains safe for future service-owned
|
||||
schemas. Depth is carried through React context because RJSF's array-item
|
||||
template does not receive a field path; context keeps objects, arrays, and
|
||||
array items in one generic hierarchy without attaching UI metadata to the
|
||||
server's schema.
|
||||
*/
|
||||
const CONFIGURATION_LAYER_COLORS = [
|
||||
'#0ea5e9', // sky
|
||||
'#6366f1', // indigo
|
||||
'#a855f7', // purple
|
||||
'#14b8a6', // teal
|
||||
'#f59e0b', // amber
|
||||
'#ec4899', // pink
|
||||
];
|
||||
const ConfigurationLayerDepthContext = createContext(0);
|
||||
|
||||
function useConfigurationLayer() {
|
||||
const depth = useContext(ConfigurationLayerDepthContext);
|
||||
return {
|
||||
color: CONFIGURATION_LAYER_COLORS[depth % CONFIGURATION_LAYER_COLORS.length],
|
||||
depth,
|
||||
};
|
||||
}
|
||||
|
||||
function buildUiSchema(schema, path = '') {
|
||||
/*
|
||||
`writeOnly` is standard JSON Schema metadata and is the sole reason a field
|
||||
@@ -151,6 +177,7 @@ function ConfigurationObjectTemplate({ description, fieldPathId, properties, tit
|
||||
const visibleProperties = properties.filter((property) => !property.hidden);
|
||||
const propertyLines = visibleProperties.map((property) => property.content);
|
||||
const topLevel = fieldPathId.path.length === 1;
|
||||
const layer = useConfigurationLayer();
|
||||
|
||||
if (fieldPathId.path.length === 0) {
|
||||
// The outer configuration card already names the root document. Rendering
|
||||
@@ -160,7 +187,11 @@ function ConfigurationObjectTemplate({ description, fieldPathId, properties, tit
|
||||
return (
|
||||
<div>
|
||||
{description ? <div className="configuration-root-description">{description}</div> : null}
|
||||
<div className="configuration-tree">{propertyLines}</div>
|
||||
{/* The invisible schema root establishes depth zero. Every visible
|
||||
structural CardFrame below it advances the context by one level. */}
|
||||
<ConfigurationLayerDepthContext.Provider value={0}>
|
||||
<div className="configuration-tree">{propertyLines}</div>
|
||||
</ConfigurationLayerDepthContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -180,23 +211,27 @@ function ConfigurationObjectTemplate({ description, fieldPathId, properties, tit
|
||||
return (
|
||||
<CardFrame
|
||||
title={title}
|
||||
color={layer.color}
|
||||
clipOverflow={false}
|
||||
className={`configuration-card${topLevel ? ' configuration-top-level-card' : ''}`}
|
||||
headerClassName={topLevel ? '!bg-sky-950' : ''}
|
||||
bodyClassName="configuration-card-body"
|
||||
>
|
||||
{description ? <div className="configuration-branch-description">{description}</div> : null}
|
||||
<div className="configuration-children">{propertyLines}</div>
|
||||
<ConfigurationLayerDepthContext.Provider value={layer.depth + 1}>
|
||||
<div className="configuration-children">{propertyLines}</div>
|
||||
</ConfigurationLayerDepthContext.Provider>
|
||||
</CardFrame>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfigurationArrayItemTemplate({ buttonsProps, children, hasToolbar, index }) {
|
||||
const unavailable = buttonsProps.disabled || buttonsProps.readonly;
|
||||
const layer = useConfigurationLayer();
|
||||
|
||||
return (
|
||||
<CardFrame
|
||||
title={`Item ${index + 1}`}
|
||||
color={layer.color}
|
||||
clipOverflow={false}
|
||||
className="configuration-card configuration-array-item"
|
||||
bodyClassName="configuration-card-body"
|
||||
@@ -220,21 +255,24 @@ function ConfigurationArrayItemTemplate({ buttonsProps, children, hasToolbar, in
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="configuration-children">{children}</div>
|
||||
<ConfigurationLayerDepthContext.Provider value={layer.depth + 1}>
|
||||
<div className="configuration-children">{children}</div>
|
||||
</ConfigurationLayerDepthContext.Provider>
|
||||
</CardFrame>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfigurationArrayTemplate({ canAdd, disabled, fieldPathId, items, onAddClick, readonly, schema, title }) {
|
||||
const topLevel = fieldPathId.path.length === 1;
|
||||
const layer = useConfigurationLayer();
|
||||
|
||||
return (
|
||||
<CardFrame
|
||||
title={title}
|
||||
meta={`${items.length} ${items.length === 1 ? 'item' : 'items'}`}
|
||||
color={layer.color}
|
||||
clipOverflow={false}
|
||||
className={`configuration-card configuration-array${topLevel ? ' configuration-top-level-card' : ''}`}
|
||||
headerClassName={topLevel ? '!bg-sky-950' : ''}
|
||||
bodyClassName="configuration-card-body"
|
||||
>
|
||||
{schema.description ? <div className="configuration-branch-description">{schema.description}</div> : null}
|
||||
@@ -245,9 +283,11 @@ function ConfigurationArrayTemplate({ canAdd, disabled, fieldPathId, items, onAd
|
||||
<button type="button" className="button-dark text-xs" disabled={disabled || readonly} onClick={onAddClick}>Add item</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="configuration-children">
|
||||
{items.length ? items : <p className="text-xs text-slate-500">No items configured.</p>}
|
||||
</div>
|
||||
<ConfigurationLayerDepthContext.Provider value={layer.depth + 1}>
|
||||
<div className="configuration-children">
|
||||
{items.length ? items : <p className="text-xs text-slate-500">No items configured.</p>}
|
||||
</div>
|
||||
</ConfigurationLayerDepthContext.Provider>
|
||||
</CardFrame>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,14 @@
|
||||
@apply min-w-0;
|
||||
}
|
||||
|
||||
.configuration-card .configuration-card {
|
||||
/* Indent the next structural boundary itself, not every row owned by the
|
||||
parent. Since nested CardFrames repeat this rule, the offset accumulates
|
||||
naturally at each level just like indentation in a YAML document. */
|
||||
margin-left: 1rem;
|
||||
width: calc(100% - 1rem);
|
||||
}
|
||||
|
||||
.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,
|
||||
@@ -22,10 +30,10 @@
|
||||
}
|
||||
|
||||
.configuration-children {
|
||||
/* Each structural card contributes one visible indentation level. Repeating
|
||||
this simple rule through nested cards mirrors YAML depth without restoring
|
||||
the custom guide lines and pseudo-card styling removed earlier. */
|
||||
@apply ml-2 space-y-0.5;
|
||||
/* Scalar rows belong directly to their current CardFrame and therefore do
|
||||
not receive blanket indentation. Nested CardFrames handle their own offset
|
||||
in the rule above, keeping keys aligned with their owning section. */
|
||||
@apply space-y-0.5;
|
||||
}
|
||||
|
||||
.configuration-line {
|
||||
@@ -44,7 +52,9 @@
|
||||
.configuration-branch-description,
|
||||
.configuration-item-description,
|
||||
.configuration-value-description {
|
||||
@apply block text-[0.7rem] leading-tight text-slate-500;
|
||||
/* Configuration guidance must remain comfortably readable; these are often
|
||||
full operational explanations rather than disposable form hints. */
|
||||
@apply block text-sm leading-snug text-slate-300;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export default function CardFrame({
|
||||
title = '',
|
||||
meta = null,
|
||||
actions = null,
|
||||
color = null,
|
||||
hideHeader = false,
|
||||
className = '',
|
||||
headerClassName = '',
|
||||
@@ -50,27 +51,26 @@ export default function CardFrame({
|
||||
const rover = roster.find((entry) => String(entry?.id) === roverId);
|
||||
return rover?.color || null;
|
||||
});
|
||||
const accentRgb = hexToRgb(ownRoverColor);
|
||||
|
||||
// swap these to toggle rover card border colors stuff
|
||||
// Green mode is global server chrome, so it wins over the assigned rover's
|
||||
// personal accent while active. Keeping this override in CardFrame makes all
|
||||
// present and future cards participate without sprinkling mode checks around.
|
||||
|
||||
// Callers can supply a structural accent, while ordinary cards continue to
|
||||
// inherit the assigned rover's color without needing to know session state.
|
||||
const accentRgb = hexToRgb(color || ownRoverColor);
|
||||
|
||||
const cardStyle = greenMode
|
||||
? { borderColor: '#008a35' }
|
||||
: accentRgb
|
||||
? { borderColor: rgba(accentRgb, 0.3) }
|
||||
: undefined;
|
||||
// const cardStyle = undefined;
|
||||
|
||||
|
||||
const headerStyle = greenMode
|
||||
? { borderColor: '#008a35' }
|
||||
: accentRgb
|
||||
? {
|
||||
// backgroundImage: `linear-gradient(90deg, rgba(23,23,23,0.96) 0%, rgba(38,38,38,0.94) 0%, ${rgba(accentRgb, 0.1)} 100%)`,
|
||||
// The header divider uses the same accent as the outside border. This
|
||||
// makes the color describe the complete CardFrame rather than looking
|
||||
// like an unrelated tint applied only behind its title.
|
||||
borderColor: rgba(accentRgb, 0.3),
|
||||
backgroundImage: `linear-gradient(90deg, ${rgba(accentRgb, 0.2)} 100%)`,
|
||||
// backgroundImage: `background-color: ${rgba(accentRgb, 0.2)}`
|
||||
}
|
||||
: undefined;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user