spacing is three bands, not one gutter
Top Nav, Sections and Footer each get their own horizontal padding, plus the one space that band owns: the room under the rule, the gap between bands, the room above the footer. The preview marker now names its band, so a change moves one band and not all three. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+56
-16
@@ -575,38 +575,78 @@ export function paintMeasure(document: Document): () => void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The site's gutter, applied to a rendered page before it is saved.
|
* The site's spacing, applied to a rendered page before it is saved.
|
||||||
*
|
*
|
||||||
* ── Why this can be classes and not variables ──
|
* ── Why this can be classes and not variables ──
|
||||||
*
|
*
|
||||||
* Colours and typefaces go into the frame as CSS custom properties, because that is what the page reads
|
* Colours and typefaces go into the frame as CSS custom properties, because that is what the page reads
|
||||||
* them as. A gutter is Tailwind classes — `px-4 md:px-8` — because only a class can carry a breakpoint,
|
* them as. Spacing is Tailwind classes — `px-4 md:px-8` — because only a class can carry a breakpoint,
|
||||||
* and there is no variable to set instead.
|
* and there is no variable to set instead.
|
||||||
*
|
*
|
||||||
* That works here for one reason: the classes are in the SITE's stylesheet, which the frame already
|
* That works here for one reason: the classes are in the SITE's stylesheet, which the frame already
|
||||||
* has. The site safelists every step (`@source inline` in styles.css), so any value this writes is one
|
* has. The site safelists every step (`@source inline` in styles.css), so any value this writes is one
|
||||||
* the frame can already render. A class invented here would silently do nothing.
|
* the frame can already render. A class invented here would silently do nothing.
|
||||||
*
|
*
|
||||||
* ── Only gutter classes are touched ──
|
* ── Named bands, and why that replaced one gutter ──
|
||||||
*
|
*
|
||||||
* The elements carrying `data-gutter` hold the gutter and nothing else that looks like it: a section's
|
* Every marked element used to take the same `px-*`. Now the header, the bands and the footer each have
|
||||||
* own padding lands on the <section>, and the band padding on <header>. So removing everything matching
|
* their own, so the marker carries which one it is — `data-gutter="header"`, `data-space="footer"` —
|
||||||
* `px-N` from these three is safe, where doing it to an arbitrary element would not be.
|
* and this rewrites one band without touching the other two.
|
||||||
|
*
|
||||||
|
* ── Only the matching utility is touched ──
|
||||||
|
*
|
||||||
|
* Each target has a pattern naming exactly what this may remove: `px-*` from a gutter, `mb-*` from the
|
||||||
|
* header, `space-y-*` from the bands. Everything else on the element survives, which is what makes it
|
||||||
|
* safe to do this to a `<header>` carrying a border and a background as well.
|
||||||
|
*
|
||||||
|
* The one exception is a SECTION, whose own padding is also a `px-*` on the marked element — so while
|
||||||
|
* the panel is open its padding reads as the site gutter. Harmless on the layout page, whose bands have
|
||||||
|
* none, and that page is the only one this runs against.
|
||||||
*/
|
*/
|
||||||
export function applyGutter(document: Document, classes: string): () => void {
|
export type SpacingDraft = {
|
||||||
const targets = [...document.querySelectorAll<HTMLElement>('[data-gutter]')];
|
header?: { paddingX?: Steps; marginBottom?: Steps };
|
||||||
const before = targets.map((element) => element.className);
|
sections?: { paddingX?: Steps; gap?: Steps };
|
||||||
|
footer?: { paddingX?: Steps; marginTop?: Steps };
|
||||||
|
};
|
||||||
|
|
||||||
const GUTTER = /^(?:md:|lg:)?px-\d+$/;
|
export type Steps = Partial<Record<'base' | 'md' | 'lg', number>>;
|
||||||
|
|
||||||
for (const element of targets) {
|
/** Mirrors `scale` in the site's layout-settings.ts. Same reason as themeProperties: a copy that shows
|
||||||
const kept = element.className.split(/\s+/).filter((name) => name && !GUTTER.test(name));
|
* a change before it is saved. If the live preview and the saved page disagree, this one is wrong. */
|
||||||
element.className = [...kept, ...classes.split(/\s+/).filter(Boolean)].join(' ');
|
export const scaleClasses = (utility: string, steps: Steps | undefined): string => {
|
||||||
|
if (!steps) return '';
|
||||||
|
const prefix = { base: '', md: 'md:', lg: 'lg:' } as const;
|
||||||
|
return (['base', 'md', 'lg'] as const)
|
||||||
|
.filter((breakpoint) => typeof steps[breakpoint] === 'number')
|
||||||
|
.map((breakpoint) => `${prefix[breakpoint]}${utility}-${steps[breakpoint]}`)
|
||||||
|
.join(' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** One selector, the utility it owns, and the classes to put there. */
|
||||||
|
const TARGETS = [
|
||||||
|
{ selector: '[data-gutter="header"]', utility: 'px', read: (s: SpacingDraft) => s.header?.paddingX },
|
||||||
|
{ selector: '[data-gutter="sections"]', utility: 'px', read: (s: SpacingDraft) => s.sections?.paddingX },
|
||||||
|
{ selector: '[data-gutter="footer"]', utility: 'px', read: (s: SpacingDraft) => s.footer?.paddingX },
|
||||||
|
{ selector: '[data-space="header"]', utility: 'mb', read: (s: SpacingDraft) => s.header?.marginBottom },
|
||||||
|
{ selector: '[data-space="sections"]', utility: 'space-y', read: (s: SpacingDraft) => s.sections?.gap },
|
||||||
|
{ selector: '[data-space="footer"]', utility: 'mt', read: (s: SpacingDraft) => s.footer?.marginTop },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function applySpacing(document: Document, spacing: SpacingDraft): () => void {
|
||||||
|
const touched: Array<[HTMLElement, string]> = [];
|
||||||
|
|
||||||
|
for (const target of TARGETS) {
|
||||||
|
const pattern = new RegExp(`^(?:md:|lg:)?${target.utility}-\\d+$`);
|
||||||
|
const classes = scaleClasses(target.utility, target.read(spacing)).split(/\s+/).filter(Boolean);
|
||||||
|
|
||||||
|
for (const element of document.querySelectorAll<HTMLElement>(target.selector)) {
|
||||||
|
touched.push([element, element.className]);
|
||||||
|
const kept = element.className.split(/\s+/).filter((name) => name && !pattern.test(name));
|
||||||
|
element.className = [...kept, ...classes].join(' ');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
targets.forEach((element, index) => {
|
for (const [element, before] of touched) element.className = before;
|
||||||
element.className = before[index]!;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
import { useDevice } from '../DeviceToggle';
|
|
||||||
/**
|
|
||||||
* The site's horizontal gutter: how far the content sits from the edge of the window.
|
|
||||||
*
|
|
||||||
* ── One setting, three places ──
|
|
||||||
*
|
|
||||||
* The header row, the content column and the footer's inner block all share the site's measure, and all
|
|
||||||
* three want the same space at the edges. That is what makes this a GUTTER rather than three paddings
|
|
||||||
* that happen to agree — and setting it once is the only arrangement in which they cannot drift.
|
|
||||||
*
|
|
||||||
* A section's own padding still exists and composes on top of this. This is the floor everything starts
|
|
||||||
* from; `PaddingTab` is the per-band control, and it stays for the pages.
|
|
||||||
*
|
|
||||||
* ── All three widths at once, and NOT tied to the device toggle ──
|
|
||||||
*
|
|
||||||
* The per-band control does it the other way: one set of inputs writing to whichever width the preview
|
|
||||||
* is showing. That is right when there are two axes and three bands, where showing everything would be
|
|
||||||
* eighteen inputs.
|
|
||||||
*
|
|
||||||
* Here there is one axis and one value, so all three fit on a row — and seeing them together is the
|
|
||||||
* point. A gutter is a progression, 4 then 8 then 12, and it is judged as one: reading it off three
|
|
||||||
* separate visits to the same control is how a site ends up tighter on a tablet than on a phone.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type Gutter = Partial<Record<'base' | 'md' | 'lg', number>>;
|
|
||||||
|
|
||||||
/** The device toggle's names, and the breakpoint each one writes. */
|
|
||||||
const BREAKPOINT = { desktop: 'lg', tablet: 'md', mobile: 'base' } as const;
|
|
||||||
|
|
||||||
/** What a width falls back to: the nearest smaller one that has a value. */
|
|
||||||
const inherited = (gutter: Gutter, key: 'base' | 'md' | 'lg'): number | null => {
|
|
||||||
const below = { base: [], md: ['base'], lg: ['md', 'base'] } as const;
|
|
||||||
for (const smaller of below[key]) {
|
|
||||||
const step = gutter[smaller];
|
|
||||||
if (typeof step === 'number') return step;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GutterTab = ({
|
|
||||||
gutter,
|
|
||||||
steps,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
gutter: Gutter;
|
|
||||||
/** The scale, sent by the API so this file holds no copy of it. */
|
|
||||||
steps: number[];
|
|
||||||
onChange: (gutter: Gutter) => void;
|
|
||||||
}) => {
|
|
||||||
const [device] = useDevice();
|
|
||||||
const key = BREAKPOINT[device];
|
|
||||||
|
|
||||||
const set = (value: string) => {
|
|
||||||
const next = { ...gutter };
|
|
||||||
// Removed rather than stored as null: an absence is what "inherit" means.
|
|
||||||
if (value === '') delete next[key];
|
|
||||||
else next[key] = Number(value);
|
|
||||||
onChange(next);
|
|
||||||
};
|
|
||||||
|
|
||||||
const own = gutter[key];
|
|
||||||
const from = inherited(gutter, key);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<h4 className="text-xs font-medium">Top Nav</h4>
|
|
||||||
<label className="text-xs text-muted-foreground">Horizontal padding</label>
|
|
||||||
<select
|
|
||||||
value={own === undefined ? '' : String(own)}
|
|
||||||
onChange={(event) => set(event.currentTarget.value)}
|
|
||||||
className="h-8 rounded-md border border-input bg-transparent px-2 text-xs"
|
|
||||||
>
|
|
||||||
{/* Mobile is the smallest, so there is nothing below it to inherit from. */}
|
|
||||||
<option value="">{key === 'base' ? 'None' : from === null ? 'Inherit (none)' : `Inherit (${from})`}</option>
|
|
||||||
{steps.map((step) => (
|
|
||||||
<option key={step} value={String(step)}>
|
|
||||||
{step}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -6,12 +6,12 @@ import { readableError } from '../errors';
|
|||||||
import { useFlash } from '../useFlash';
|
import { useFlash } from '../useFlash';
|
||||||
import { useSaveTarget } from '../SaveTarget';
|
import { useSaveTarget } from '../SaveTarget';
|
||||||
import { usePreviewable, useViewMode } from '../ViewMode';
|
import { usePreviewable, useViewMode } from '../ViewMode';
|
||||||
import { applyGutter, paintMeasure } from '../canvas';
|
import { applySpacing, paintMeasure, type SpacingDraft } from '../canvas';
|
||||||
import { SitePreview, useDevPort } from '../SitePreview';
|
import { SitePreview, useDevPort } from '../SitePreview';
|
||||||
import { DeviceToggle } from '../DeviceToggle';
|
import { DeviceToggle } from '../DeviceToggle';
|
||||||
import { useEditorSlot } from '../useEditorSlot';
|
import { useEditorSlot } from '../useEditorSlot';
|
||||||
import { type LayoutBand } from './PaddingTab';
|
import { type LayoutBand } from './PaddingTab';
|
||||||
import { GutterTab, type Gutter } from './GutterTab';
|
import { SpacingTab } from './SpacingTab';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The shell every page renders inside: the menu, the footer, the colours and the typefaces.
|
* The shell every page renders inside: the menu, the footer, the colours and the typefaces.
|
||||||
@@ -59,8 +59,8 @@ type Layout = {
|
|||||||
sections: LayoutBand[];
|
sections: LayoutBand[];
|
||||||
/** The steps a padding may take, sent by the API so this file holds no copy of the scale. */
|
/** The steps a padding may take, sent by the API so this file holds no copy of the scale. */
|
||||||
padding_steps: number[];
|
padding_steps: number[];
|
||||||
/** The site's horizontal gutter, per breakpoint. */
|
/** Where the space goes, band by band. See SpacingTab, and the site's own spacingClasses. */
|
||||||
padding_x: Gutter;
|
spacing: SpacingDraft;
|
||||||
/**
|
/**
|
||||||
* Which face plays each role when nothing has been chosen, read from the site's own stylesheet.
|
* Which face plays each role when nothing has been chosen, read from the site's own stylesheet.
|
||||||
*
|
*
|
||||||
@@ -111,21 +111,6 @@ const themeProperties = (draft: Layout): Record<string, string | null> => ({
|
|||||||
'--font-display': draft.font_display && `"${draft.font_display}", sans-serif`,
|
'--font-display': draft.font_display && `"${draft.font_display}", sans-serif`,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* The gutter as the classes the site would emit.
|
|
||||||
*
|
|
||||||
* Mirrors `gutterClasses` in the site's layout-settings.ts, and is a copy for the same reason
|
|
||||||
* `themeProperties` is: showing a change before it is saved means producing it here. The check is the
|
|
||||||
* same — save, and if the live preview and the saved page disagree, this copy is the wrong one.
|
|
||||||
*/
|
|
||||||
const gutterClasses = (gutter: Gutter): string => {
|
|
||||||
const prefix = { base: '', md: 'md:', lg: 'lg:' } as const;
|
|
||||||
return (['base', 'md', 'lg'] as const)
|
|
||||||
.filter((breakpoint) => typeof gutter[breakpoint] === 'number')
|
|
||||||
.map((breakpoint) => `${prefix[breakpoint]}px-${gutter[breakpoint]}`)
|
|
||||||
.join(' ');
|
|
||||||
};
|
|
||||||
|
|
||||||
const COLOURS = [
|
const COLOURS = [
|
||||||
{ key: 'background' as const, label: 'Background', fallback: '#ffffff' },
|
{ key: 'background' as const, label: 'Background', fallback: '#ffffff' },
|
||||||
{ key: 'foreground' as const, label: 'Text', fallback: '#000000' },
|
{ key: 'foreground' as const, label: 'Text', fallback: '#000000' },
|
||||||
@@ -187,7 +172,7 @@ export const LayoutPanel = () => {
|
|||||||
made in a build script or a stylesheet.
|
made in a build script or a stylesheet.
|
||||||
*/
|
*/
|
||||||
const WRITABLE = [
|
const WRITABLE = [
|
||||||
'padding_x',
|
'spacing',
|
||||||
'background',
|
'background',
|
||||||
'foreground',
|
'foreground',
|
||||||
'accent',
|
'accent',
|
||||||
@@ -234,24 +219,26 @@ export const LayoutPanel = () => {
|
|||||||
at the page.
|
at the page.
|
||||||
*/
|
*/
|
||||||
/*
|
/*
|
||||||
The measure, and the gutter being drafted.
|
The measure, and the spacing being drafted.
|
||||||
|
|
||||||
`gutter` is in the dependencies, so changing it hands SitePreview a new function: its effect runs
|
`spacingKey` is in the dependencies, so changing a number hands SitePreview a new function: its
|
||||||
the previous application's cleanup — which puts the original classes back — and then applies the new
|
effect runs the previous application's cleanup — which puts the original classes back — and then
|
||||||
ones. A padding shows in the page as it is chosen, like a colour does, with no save and no reload.
|
applies the new ones. A gap shows in the page as it is chosen, like a colour does, with no save.
|
||||||
*/
|
*/
|
||||||
const gutter = draft ? gutterClasses(draft.padding_x) : '';
|
// Serialised, so the callback is rebuilt when a NUMBER changes rather than when the object identity
|
||||||
|
// does — a fresh `{}` every render would re-apply the whole preview on every keystroke elsewhere.
|
||||||
|
const spacingKey = JSON.stringify(draft?.spacing ?? {});
|
||||||
|
|
||||||
const onDocument = useCallback(
|
const onDocument = useCallback(
|
||||||
(frameDocument: Document) => {
|
(frameDocument: Document) => {
|
||||||
const measure = paintMeasure(frameDocument);
|
const measure = paintMeasure(frameDocument);
|
||||||
const spacing = applyGutter(frameDocument, gutter);
|
const space = applySpacing(frameDocument, JSON.parse(spacingKey) as SpacingDraft);
|
||||||
return () => {
|
return () => {
|
||||||
spacing();
|
space();
|
||||||
measure();
|
measure();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[gutter],
|
[spacingKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [, setEditor] = useEditorSlot();
|
const [, setEditor] = useEditorSlot();
|
||||||
@@ -364,21 +351,11 @@ export const LayoutPanel = () => {
|
|||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<DeviceToggle size="section" />
|
<DeviceToggle size="section" />
|
||||||
</div>
|
</div>
|
||||||
{/*
|
{/* Three bands. PaddingTab is still the per-SECTION control, for the pages. */}
|
||||||
The site's gutter, not a per-band padding.
|
<SpacingTab
|
||||||
|
spacing={draft.spacing}
|
||||||
PaddingTab is the per-band control and it stays — for the pages, where a band really
|
|
||||||
does want its own spacing. Here the three wrappers share one measure and should share
|
|
||||||
one number.
|
|
||||||
|
|
||||||
All three widths at once, and deliberately NOT tied to the device toggle: a gutter is a
|
|
||||||
progression, 4 then 8 then 12, and it is judged as one. Reading it off three separate
|
|
||||||
visits to the same control is how a site ends up tighter on a tablet than on a phone.
|
|
||||||
*/}
|
|
||||||
<GutterTab
|
|
||||||
gutter={draft.padding_x}
|
|
||||||
steps={draft.padding_steps}
|
steps={draft.padding_steps}
|
||||||
onChange={(padding_x) => patch({ padding_x })}
|
onChange={(spacing) => patch({ spacing })}
|
||||||
/>
|
/>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { useDevice } from '../DeviceToggle';
|
||||||
|
import type { SpacingDraft, Steps } from '../canvas';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where the space goes: three bands, each with its own numbers.
|
||||||
|
*
|
||||||
|
* It was one gutter for the whole site, on the argument that the header, the bands and the footer hold
|
||||||
|
* the same measure and should not drift. They do hold the same measure — and they still want different
|
||||||
|
* edges, because the menu is not the page. So the guarantee moved: three values, chosen in one place,
|
||||||
|
* side by side.
|
||||||
|
*
|
||||||
|
* ── One value per field ──
|
||||||
|
*
|
||||||
|
* The device toggle above says which width is being edited. Showing all three would be eighteen
|
||||||
|
* selects, seventeen of them for a screen you are not looking at.
|
||||||
|
*
|
||||||
|
* Unset inherits the smaller width, so "the same everywhere" is one choice. Zero is the other
|
||||||
|
* instruction — none HERE even though the phone has some — which is why they are separate options.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BREAKPOINT = { desktop: 'lg', tablet: 'md', mobile: 'base' } as const;
|
||||||
|
|
||||||
|
/** The nearest smaller width that has a value, or null. */
|
||||||
|
const inherited = (steps: Steps | undefined, key: 'base' | 'md' | 'lg'): number | null => {
|
||||||
|
const below = { base: [], md: ['base'], lg: ['md', 'base'] } as const;
|
||||||
|
for (const smaller of below[key]) {
|
||||||
|
const step = steps?.[smaller];
|
||||||
|
if (typeof step === 'number') return step;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The vocabulary. A knob is a line here, the class it emits on the site, and a field in the API. */
|
||||||
|
const BANDS = [
|
||||||
|
{
|
||||||
|
key: 'header' as const,
|
||||||
|
label: 'Top Nav',
|
||||||
|
fields: [
|
||||||
|
{ key: 'paddingX' as const, label: 'Horizontal padding' },
|
||||||
|
{ key: 'marginBottom' as const, label: 'Margin bottom' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sections' as const,
|
||||||
|
label: 'Sections',
|
||||||
|
fields: [
|
||||||
|
{ key: 'paddingX' as const, label: 'Horizontal padding' },
|
||||||
|
{ key: 'gap' as const, label: 'Space between' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'footer' as const,
|
||||||
|
label: 'Footer',
|
||||||
|
fields: [
|
||||||
|
{ key: 'paddingX' as const, label: 'Horizontal padding' },
|
||||||
|
{ key: 'marginTop' as const, label: 'Margin top' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SpacingTab = ({
|
||||||
|
spacing,
|
||||||
|
steps,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
spacing: SpacingDraft;
|
||||||
|
/** The scale, sent by the API so this file holds no copy of it. */
|
||||||
|
steps: number[];
|
||||||
|
onChange: (spacing: SpacingDraft) => void;
|
||||||
|
}) => {
|
||||||
|
const [device] = useDevice();
|
||||||
|
const key = BREAKPOINT[device];
|
||||||
|
|
||||||
|
const set = (band: string, field: string, value: string) => {
|
||||||
|
const bands = spacing as Record<string, Record<string, Steps> | undefined>;
|
||||||
|
const at: Steps = { ...(bands[band]?.[field] ?? {}) };
|
||||||
|
// Removed rather than stored as null: an absence is what "inherit" means, all the way up.
|
||||||
|
if (value === '') delete at[key];
|
||||||
|
else at[key] = Number(value);
|
||||||
|
|
||||||
|
const nextBand = { ...(bands[band] ?? {}) };
|
||||||
|
if (Object.keys(at).length) nextBand[field] = at;
|
||||||
|
else delete nextBand[field];
|
||||||
|
|
||||||
|
const next = { ...bands };
|
||||||
|
if (Object.keys(nextBand).length) next[band] = nextBand;
|
||||||
|
else delete next[band];
|
||||||
|
|
||||||
|
onChange(next as SpacingDraft);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{BANDS.map((band) => (
|
||||||
|
<div key={band.key} className="flex flex-col gap-2">
|
||||||
|
<h4 className="text-xs font-medium">{band.label}</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{band.fields.map((field) => {
|
||||||
|
const current = (spacing as Record<string, Record<string, Steps> | undefined>)[band.key]?.[field.key];
|
||||||
|
const own = current?.[key];
|
||||||
|
const from = inherited(current, key);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={field.key} className="flex flex-col gap-1">
|
||||||
|
<label className="text-xs text-muted-foreground">{field.label}</label>
|
||||||
|
<select
|
||||||
|
value={own === undefined ? '' : String(own)}
|
||||||
|
onChange={(event) => set(band.key, field.key, event.currentTarget.value)}
|
||||||
|
className="h-8 rounded-md border border-input bg-transparent px-2 text-xs"
|
||||||
|
>
|
||||||
|
{/* Mobile is the smallest, so there is nothing below it to inherit from. */}
|
||||||
|
<option value="">
|
||||||
|
{key === 'base' ? 'None' : from === null ? 'Inherit (none)' : `Inherit (${from})`}
|
||||||
|
</option>
|
||||||
|
{steps.map((step) => (
|
||||||
|
<option key={step} value={String(step)}>
|
||||||
|
{step}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user