mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Move three forms onto shared controls and draw the select arrow (#2546)
This clears the shared template audit (`node
scripts/shared-template-audit.mjs`) for the forms that still built their
own controls. Three findings remain, in BackupTransferDialogs.tsx and
AlertDeadManDestinationSection.tsx. Those two files are part of an open
`bg-base` change and will follow after it lands.
**Moved onto shared controls:**
- **Alerts › Thresholds › CPU averaging:** the ten per-platform selects
now use `FormSelect`, with each profile name as a visible label.
- **Discovery availability proposal:** the Interval and Observation
location fields use `FormSelect`, and the check-name field next to them
uses the shared field classes, so the three match.
- **Availability check › Request body:** the field uses `FormTextarea`.
Its size note is now the field's accessible description. With
`sm:min-h-24` it also gets the 96px height its class asked for on
desktop; on main it rendered at 54px.
**Select arrow.** `FormSelect`'s default chrome set `appearance-none`
and drew no arrow of its own, so every default select in Settings read
as a text field. That includes the existing Preset, Target type and
Probe selects in the availability dialog. A new `form-select-arrow`
utility draws a caret in the padding the class already reserved, using
the muted text colour. In forced-colors mode it hands the native arrow
back. Filter-bar selects use their own class and are unchanged.
**Registry fixes:**
- `AlertEscalationSection` is removed from the `form-select-shell`
required consumers. It has used checkboxes since e0baecc418.
- The inline detail row rule now excludes test files. Its only finding
was `ProxmoxBackupsTable.test.tsx`.
Verification:
- Checked live on a mock stack, before and after, in light and dark mode
at 1440px and 390px.
- The Discovery card was rendered through route interception.
- Forced-colors mode was emulated.
- 11,393 tests pass, and the receipt is included.
This commit is contained in:
@@ -2928,7 +2928,8 @@ hours colour the dashboard and never alert. A group switched off as a whole
|
||||
shows its Disabled badge and no defaults line. The CPU averaging line counts
|
||||
every configured profile override, including ones only the API sets. CPU averaging is tuning, not the page's
|
||||
answer. It is one collapsed disclosure stating the effective window and the
|
||||
number of platform overrides, with the same per-profile selects inside. Docker
|
||||
number of platform overrides, with the same per-profile selects inside, each
|
||||
a compact shared `FormSelect` with its profile name as a visible label. Docker
|
||||
and Machines list their resource groups before Docker-only rules and the
|
||||
SMART card. Threshold values, inheritance, metric evaluation windows and
|
||||
persistence are unchanged.
|
||||
|
||||
@@ -1822,6 +1822,10 @@ payload shape change when the portal presents compact client rows.
|
||||
arbitrary remote content. Contract tests must pin redaction, unchanged-
|
||||
secret preservation, bounded evaluation, and the reachable-but-incorrect
|
||||
application case.
|
||||
The settings client edits the request body in the shared `FormTextarea`,
|
||||
whose size note (`Up to 8 KiB`) is the field's accessible description;
|
||||
removing a stored body stays an explicit action beside it, and leaving the
|
||||
field blank keeps the stored body as before.
|
||||
Availability target protocol vocabulary is canonicalized at the API/config
|
||||
boundary: clients may submit `ping` as a user-facing alias for the ICMP
|
||||
check, but saved targets, API reads, probe status, connections rows, and
|
||||
|
||||
@@ -2464,6 +2464,12 @@ not a replacement status card, CTA band, or page-local nested card.
|
||||
apply controlled `value` props after options are mounted so settings panels
|
||||
such as Discovery show the persisted option instead of falling back
|
||||
to the first option while the collapsed summary shows a different value.
|
||||
Its default select chrome (`formSelect` / `formSelectCompact` in
|
||||
`Form.ts`) sets `appearance-none` to match the text inputs, so it also
|
||||
carries the `form-select-arrow` utility, which draws the dropdown caret in
|
||||
the reserved right padding (and hands the native arrow back in forced-colors
|
||||
mode); without it every default `FormSelect` read as a text field. Callers
|
||||
that pass their own `selectBaseClass` keep their own arrow.
|
||||
The Assistant runtime controls in
|
||||
`frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx` — e.g. the
|
||||
service context scan `Toggle` — are settings-shell chrome bound to the canonical
|
||||
|
||||
@@ -2907,7 +2907,9 @@ Discovery-led availability proposals present their evidence and canonical
|
||||
attachment boundary as short, complete sentences in both the inline card and
|
||||
machine-wide review. The colocated component test pins that operator-facing
|
||||
copy so the shared copy-style audit cannot regress it back to semicolon-joined
|
||||
instructions.
|
||||
instructions. The card's editable check name, interval and observation
|
||||
location use the shared form label and control chrome (`FormSelect` for the
|
||||
two choices), so they match the availability settings form they hand off to.
|
||||
|
||||
### Provider workflows hydrate only their active route inventory
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "979466013876ce54a67494b0c4cda9b404ea45e8",
|
||||
"verified_at": "2026-10-05T22:01:46Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/components/Discovery/AvailabilityProposalCard.tsx",
|
||||
"frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx",
|
||||
"frontend-modern/src/components/shared/Form.ts",
|
||||
"frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx",
|
||||
"frontend-modern/src/index.css"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/components/Discovery/AvailabilityProposalCard.tsx": "bfa50b13f521b9a91b57495eec3a827a71a413a0eed0832be0d58444e8ca3ee7",
|
||||
"frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx": "af7a1d322045c33747600466a580ae6148f752c8d847404a6f5b04fa49fd53e7",
|
||||
"frontend-modern/src/components/shared/Form.ts": "e47d06ebbbba68ac0d19769acac83a0ca930b1e2d5a7258c7c1fcf43e82ce44a",
|
||||
"frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx": "8142c12c2e6930a505e1f063aa8a396bfa45c6c55a8ea7c7d1085198a9f3915f",
|
||||
"frontend-modern/src/index.css": "b8ae1a40f0915ff034e51a16a94e0c2159ac726ef0afdfdd8fb321f5d5f7dc5e"
|
||||
},
|
||||
"routes": [
|
||||
"/alerts/thresholds",
|
||||
"/settings/monitoring/availability",
|
||||
"/proxmox/overview"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 1100
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 1100
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Alerts > Thresholds > CPU averaging opened: all 10 per-platform selects render through FormSelect with their visible label tied by for/id, values intact (5-minute average, Inherit), 32px desktop / 44px phone, and a dropdown caret; main showed native selects",
|
||||
"Settings > Availability checks > Add service/device check with an HTTP POST probe: Request body (optional) is a FormTextarea with its label tied and the 8 KiB help linked by aria-describedby, 96px tall on desktop and phone (main: 54px on desktop because formControl's sm:min-h-9 overrode min-h-24; no help link)",
|
||||
"Existing default FormSelects in that dialog (Preset, Target type, Probe) now show a caret; on main they had appearance-none and no arrow, reading as text fields",
|
||||
"Proxmox guest drawer Discovery tab with discovery_enabled and an availability proposal served by route interception: Check name input and the Interval / Observation location FormSelects share the shared form label and control chrome at matched heights, with carets",
|
||||
"Dark mode at 1440: caret drawn in the muted text colour and readable; forced-colors emulation: select falls back to appearance auto with no background image",
|
||||
"Shared template audit: only the three findings in BackupTransferDialogs.tsx and AlertDeadManDestinationSection.tsx remain (held by another session's open bg-base change)",
|
||||
"Re-checked after rebasing onto 9794660138 (#2535, shared toggle): CPU averaging selects unchanged, labelled, with the caret"
|
||||
],
|
||||
"interactions": [
|
||||
"Opened the CPU averaging disclosure, the availability check dialog (HTTP, POST) and a guest Discovery tab, on main (reverse-applied patch) and on the change",
|
||||
"Emulated forced-colors: active"
|
||||
]
|
||||
}
|
||||
@@ -1709,7 +1709,6 @@
|
||||
{ "path": "src/components/shared/FilterToolbar.tsx" },
|
||||
{ "path": "src/features/alerts/AlertAppriseDestinationsSection.tsx" },
|
||||
{ "path": "src/features/alerts/AlertDeliveryRoutingSection.tsx" },
|
||||
{ "path": "src/features/alerts/AlertEscalationSection.tsx" },
|
||||
{ "path": "src/features/alerts/AlertQuietHoursSection.tsx" },
|
||||
{ "path": "src/features/patrol/PatrolAttentionWorkbench.tsx" }
|
||||
],
|
||||
@@ -6664,12 +6663,11 @@
|
||||
"src/features/vmware"
|
||||
],
|
||||
"extensions": [".tsx"],
|
||||
"pathExcludes": [".test.", "__tests__"],
|
||||
"triggerPatterns": ["data-inline", "TableRow"],
|
||||
"requiredPatterns": ["InlineDetailTableRow"],
|
||||
"allowedPaths": [],
|
||||
"ignoredPaths": [
|
||||
"src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx"
|
||||
],
|
||||
"ignoredPaths": [],
|
||||
"proof": [
|
||||
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
||||
"scripts/shared-template-audit.mjs"
|
||||
|
||||
@@ -304,6 +304,18 @@ describe('App architecture', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('draws the shared select arrow that appearance-none removes', () => {
|
||||
// FormSelect's default chrome sets appearance-none to match text inputs;
|
||||
// without this utility every default select read as a text field.
|
||||
expect(appStylesSource).toMatch(
|
||||
/@utility form-select-arrow \{\s*background-image:\s*linear-gradient\(45deg, transparent 50%, var\(--color-text-muted\) 50%\)/,
|
||||
);
|
||||
// Forced-colors mode drops background images, so the native arrow returns.
|
||||
expect(appStylesSource).toMatch(
|
||||
/@media \(forced-colors: active\) \{\s*appearance: auto !important;\s*background-image: none;/,
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps compact phone settings spacing scoped to the settings content shell', () => {
|
||||
expect(appStylesSource).toContain('@media (max-width: 39.999rem)');
|
||||
expect(appStylesSource).toContain(
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
} from '@/api/discovery';
|
||||
import { ActionIconButton, Button } from '@/components/shared/Button';
|
||||
import { Dialog } from '@/components/shared/Dialog';
|
||||
import { formControl, formField, formLabel } from '@/components/shared/Form';
|
||||
import { FormSelect } from '@/components/shared/FormSelect';
|
||||
import { LoadingSpinner } from '@/components/shared/LoadingSpinner';
|
||||
import type { DiscoverySummary, ResourceDiscovery, ResourceType } from '@/types/discovery';
|
||||
import {
|
||||
@@ -298,47 +300,35 @@ export const AvailabilityProposalCard: Component<AvailabilityProposalCardProps>
|
||||
</p>
|
||||
|
||||
<div class="mt-4 grid gap-3 sm:grid-cols-3">
|
||||
<label class="block sm:col-span-1">
|
||||
<span class="text-[11px] font-medium text-base-content">
|
||||
Check name · you control
|
||||
</span>
|
||||
<label class={formField}>
|
||||
<span class={formLabel}>Check name · you control</span>
|
||||
<input
|
||||
class="mt-1 w-full rounded-md border border-border bg-surface px-2.5 py-2 text-sm text-base-content"
|
||||
class={formControl}
|
||||
value={name()}
|
||||
onInput={(event) => setName(event.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-[11px] font-medium text-base-content">
|
||||
Interval · you control
|
||||
</span>
|
||||
<select
|
||||
class="mt-1 w-full rounded-md border border-border bg-surface px-2.5 py-2 text-sm text-base-content"
|
||||
value={intervalSeconds()}
|
||||
onChange={(event) => setIntervalSeconds(Number(event.currentTarget.value))}
|
||||
>
|
||||
<option value={30}>Every 30 seconds</option>
|
||||
<option value={60}>Every minute</option>
|
||||
<option value={300}>Every 5 minutes</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="text-[11px] font-medium text-base-content">
|
||||
Observation location · you control
|
||||
</span>
|
||||
<select
|
||||
class="mt-1 w-full rounded-md border border-border bg-surface px-2.5 py-2 text-sm text-base-content"
|
||||
value={probeAgentId()}
|
||||
onChange={(event) => setProbeAgentId(event.currentTarget.value)}
|
||||
>
|
||||
<option value="">This Pulse server</option>
|
||||
<For each={props.connectedAgents ?? []}>
|
||||
{(agent) => (
|
||||
<option value={agent.agent_id}>{agent.hostname || agent.agent_id}</option>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
</label>
|
||||
<FormSelect
|
||||
label="Interval · you control"
|
||||
value={intervalSeconds()}
|
||||
onChange={(event) => setIntervalSeconds(Number(event.currentTarget.value))}
|
||||
>
|
||||
<option value={30}>Every 30 seconds</option>
|
||||
<option value={60}>Every minute</option>
|
||||
<option value={300}>Every 5 minutes</option>
|
||||
</FormSelect>
|
||||
<FormSelect
|
||||
label="Observation location · you control"
|
||||
value={probeAgentId()}
|
||||
onChange={(event) => setProbeAgentId(event.currentTarget.value)}
|
||||
>
|
||||
<option value="">This Pulse server</option>
|
||||
<For each={props.connectedAgents ?? []}>
|
||||
{(agent) => (
|
||||
<option value={agent.agent_id}>{agent.hostname || agent.agent_id}</option>
|
||||
)}
|
||||
</For>
|
||||
</FormSelect>
|
||||
</div>
|
||||
|
||||
<Show when={duplicate()}>
|
||||
|
||||
+11
-12
@@ -10,6 +10,7 @@ import {
|
||||
formLabel,
|
||||
} from '@/components/shared/Form';
|
||||
import { FormSelect } from '@/components/shared/FormSelect';
|
||||
import { FormTextarea } from '@/components/shared/FormTextarea';
|
||||
import {
|
||||
AvailabilityTargetsAPI,
|
||||
type AvailabilityProbeProtocol,
|
||||
@@ -776,10 +777,11 @@ export const AvailabilityTargetSlot: Component<AvailabilityTargetSlotProps> = (p
|
||||
</div>
|
||||
|
||||
<Show when={form().httpMethod === 'POST'}>
|
||||
<label class={formField}>
|
||||
<span class={formLabel}>Request body (optional)</span>
|
||||
<textarea
|
||||
class={`${formControl} min-h-24 font-mono text-xs`}
|
||||
<div class={formField}>
|
||||
<FormTextarea
|
||||
label="Request body (optional)"
|
||||
textareaBaseClass={`${formControl} min-h-24 sm:min-h-24 font-mono text-xs`}
|
||||
help="Up to 8 KiB. Kept out of history and test output."
|
||||
value={form().httpBody}
|
||||
onInput={(event) =>
|
||||
updateForm({
|
||||
@@ -794,11 +796,8 @@ export const AvailabilityTargetSlot: Component<AvailabilityTargetSlotProps> = (p
|
||||
: '{"operation":"health"}'
|
||||
}
|
||||
/>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<span class={formHelpText}>
|
||||
Up to 8 KiB. Kept out of history and test output.
|
||||
</span>
|
||||
<Show when={form().httpBodyConfigured && !form().httpBodyTouched}>
|
||||
<Show when={form().httpBodyConfigured && !form().httpBodyTouched}>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs font-medium text-error hover:underline"
|
||||
@@ -812,9 +811,9 @@ export const AvailabilityTargetSlot: Component<AvailabilityTargetSlotProps> = (p
|
||||
>
|
||||
Remove stored body
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
|
||||
+4
@@ -338,6 +338,10 @@ describe('AvailabilityTargetSlot', () => {
|
||||
'placeholder',
|
||||
'Stored securely — leave blank to keep it',
|
||||
);
|
||||
// The request body composes FormTextarea, which links its size note.
|
||||
expect(screen.getByLabelText(/^Request body \(optional\)/)).toHaveAccessibleDescription(
|
||||
'Up to 8 KiB. Kept out of history and test output.',
|
||||
);
|
||||
expect(screen.getByLabelText('Header value')).toHaveAttribute(
|
||||
'placeholder',
|
||||
'Stored securely — leave blank to keep it',
|
||||
|
||||
@@ -25,9 +25,9 @@ export const formCheckbox = baseCheckbox;
|
||||
export const formControlDense = join(baseControl, 'py-1.5 px-2');
|
||||
export const formControlMono = join(baseControl, 'font-mono');
|
||||
|
||||
export const formSelect = join(baseControl, 'pr-8 appearance-none');
|
||||
export const formSelect = join(baseControl, 'pr-8 appearance-none form-select-arrow');
|
||||
export const formTextarea = join(baseControl, 'min-h-[120px] resize-vertical');
|
||||
export const formSelectCompact = join(compactControl, 'pr-8 appearance-none');
|
||||
export const formSelectCompact = join(compactControl, 'pr-8 appearance-none form-select-arrow');
|
||||
export const formTextareaCompact = join(compactControl, 'min-h-16 resize-y');
|
||||
|
||||
export const formLabelMuted = join(baseLabel, 'text-muted font-normal');
|
||||
|
||||
@@ -20,6 +20,8 @@ describe('FormSelect', () => {
|
||||
expect(select).toBe(screen.getByTestId('delivery-mode'));
|
||||
expect(select).toHaveValue('cli');
|
||||
expect(select).toHaveClass('min-h-11', 'sm:min-h-9');
|
||||
// appearance-none drops the native arrow, so the shared class draws one.
|
||||
expect(select).toHaveClass('appearance-none', 'form-select-arrow');
|
||||
});
|
||||
|
||||
it('preserves explicit ids and compact styling hooks', () => {
|
||||
|
||||
@@ -719,7 +719,6 @@ describe('shared primitive guardrails', () => {
|
||||
'src/components/shared/FilterToolbar.tsx',
|
||||
'src/features/alerts/AlertAppriseDestinationsSection.tsx',
|
||||
'src/features/alerts/AlertDeliveryRoutingSection.tsx',
|
||||
'src/features/alerts/AlertEscalationSection.tsx',
|
||||
'src/features/alerts/AlertQuietHoursSection.tsx',
|
||||
'src/features/patrol/PatrolAttentionWorkbench.tsx',
|
||||
]);
|
||||
@@ -7730,6 +7729,7 @@ describe('shared primitive guardrails', () => {
|
||||
requiredPatterns?: string[];
|
||||
allowedPaths?: string[];
|
||||
ignoredPaths?: string[];
|
||||
pathExcludes?: string[];
|
||||
}>;
|
||||
};
|
||||
const registeredRule = registry.rules?.find(
|
||||
@@ -7774,9 +7774,9 @@ describe('shared primitive guardrails', () => {
|
||||
expect(guard?.allowedPaths ?? []).toHaveLength(0);
|
||||
}
|
||||
expect(inlineGuard?.triggerPatterns).toEqual(['data-inline', 'TableRow']);
|
||||
expect(inlineGuard?.ignoredPaths).toEqual([
|
||||
'src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx',
|
||||
]);
|
||||
// Test files assert on rendered rows; they are not row surfaces.
|
||||
expect(inlineGuard?.pathExcludes).toEqual(['.test.', '__tests__']);
|
||||
expect(inlineGuard?.ignoredPaths).toEqual([]);
|
||||
expect(namedGuard?.triggerPatterns).toEqual(['detail-row', 'TableRow']);
|
||||
expect(namedGuard?.ignoredPaths ?? []).toHaveLength(0);
|
||||
|
||||
|
||||
@@ -379,6 +379,12 @@ describe('ThresholdsTab', () => {
|
||||
'5-minute average, 1 platform override',
|
||||
);
|
||||
expect(virtualMachineWindow.options[0].textContent).toBe('Inherit (15 minutes)');
|
||||
// The window controls compose the shared FormSelect: a visible label tied
|
||||
// to the select and the shared dropdown arrow.
|
||||
expect(document.querySelector(`label[for="${workloadWindow.id}"]`)?.textContent).toBe(
|
||||
'All workloads',
|
||||
);
|
||||
expect(workloadWindow).toHaveClass('appearance-none', 'form-select-arrow');
|
||||
|
||||
fireEvent.input(workloadWindow, { target: { value: '60' } });
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createSignal, For, Show } from 'solid-js';
|
||||
import ChevronRight from 'lucide-solid/icons/chevron-right';
|
||||
import { ThresholdsTable } from '@/components/Alerts/ThresholdsTable';
|
||||
import { FormSelect } from '@/components/shared/FormSelect';
|
||||
import {
|
||||
AlertIntentPolicyPanel,
|
||||
type AlertIntentPolicySelectionTarget,
|
||||
@@ -96,31 +97,28 @@ export function ThresholdsTab(props: ThresholdsTabProps) {
|
||||
const evaluationProfileControl = (profile: (typeof evaluationProfiles)[number]) => {
|
||||
const configured = () => props.metricEvaluationWindows?.()[profile.key]?.cpu;
|
||||
return (
|
||||
<label class="block rounded-md border border-border p-3">
|
||||
<span class="block text-xs font-medium text-base-content">{profile.label}</span>
|
||||
<select
|
||||
aria-label={`${profile.label} CPU averaging`}
|
||||
class="mt-2 w-full rounded-md border border-border bg-surface px-2 py-1.5 text-sm text-base-content"
|
||||
value={
|
||||
profile.key === 'all'
|
||||
? String(configured() ?? 300)
|
||||
: configured() === undefined
|
||||
? 'inherit'
|
||||
: String(configured())
|
||||
}
|
||||
onInput={(event) => updateCPUWindow(profile.key, event.currentTarget.value)}
|
||||
>
|
||||
<Show when={profile.key !== 'all'}>
|
||||
<option value="inherit">
|
||||
Inherit ({evaluationLabel(inheritedCPUWindow(profile))})
|
||||
</option>
|
||||
</Show>
|
||||
<option value="0">Current value</option>
|
||||
<option value="60">1-minute average</option>
|
||||
<option value="300">5-minute average</option>
|
||||
<option value="900">15-minute average</option>
|
||||
</select>
|
||||
</label>
|
||||
<FormSelect
|
||||
label={profile.label}
|
||||
aria-label={`${profile.label} CPU averaging`}
|
||||
fieldClass="rounded-md border border-border p-3"
|
||||
density="compact"
|
||||
value={
|
||||
profile.key === 'all'
|
||||
? String(configured() ?? 300)
|
||||
: configured() === undefined
|
||||
? 'inherit'
|
||||
: String(configured())
|
||||
}
|
||||
onInput={(event) => updateCPUWindow(profile.key, event.currentTarget.value)}
|
||||
>
|
||||
<Show when={profile.key !== 'all'}>
|
||||
<option value="inherit">Inherit ({evaluationLabel(inheritedCPUWindow(profile))})</option>
|
||||
</Show>
|
||||
<option value="0">Current value</option>
|
||||
<option value="60">1-minute average</option>
|
||||
<option value="300">5-minute average</option>
|
||||
<option value="900">15-minute average</option>
|
||||
</FormSelect>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -88,6 +88,28 @@
|
||||
padding-bottom: max(3.5rem, env(safe-area-inset-bottom, 3.5rem));
|
||||
}
|
||||
|
||||
/* The shared select classes drop the native arrow (appearance-none) so the
|
||||
control matches the text inputs, which left every FormSelect looking like
|
||||
a text field. Draw a small caret in the reserved right padding instead, in
|
||||
the muted text colour so it follows the theme. */
|
||||
@utility form-select-arrow {
|
||||
background-image:
|
||||
linear-gradient(45deg, transparent 50%, var(--color-text-muted) 50%),
|
||||
linear-gradient(135deg, var(--color-text-muted) 50%, transparent 50%);
|
||||
background-position:
|
||||
calc(100% - 15px) calc(50% + 1px),
|
||||
calc(100% - 10px) calc(50% + 1px);
|
||||
background-size: 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
/* Forced-colors mode drops background images, so hand the arrow back to
|
||||
the browser there. */
|
||||
@media (forced-colors: active) {
|
||||
appearance: auto !important;
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
@utility scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
Reference in New Issue
Block a user