cardframing again

This commit is contained in:
legop3
2026-09-14 13:27:48 -04:00
parent 0266bd9568
commit 9b5c187aac
18 changed files with 145 additions and 94 deletions
@@ -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>
);
}
+15 -5
View File
@@ -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;
}
+10 -10
View File
@@ -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;