mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
cardframing again
This commit is contained in:
@@ -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