mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Move three forms onto shared controls and draw the select arrow
The shared template audit listed Settings, Alerts and Discovery forms still building their own controls. The Thresholds CPU averaging selects and the Discovery availability proposal now compose FormSelect (the proposal's name field takes the shared field classes beside them), and an availability check's request body composes FormTextarea, which links its help text and, with sm:min-h-24, finally gets the height its class asked for on desktop. FormSelect's default chrome set appearance-none and drew no arrow, so every default select in Settings read as a text field. A form-select-arrow utility draws the caret in the padding the class already reserved, and hands the native arrow back in forced-colors mode. Two registry entries were wrong: AlertEscalationSection has used checkboxes instead of a select since its destinations became per-escalation, and the inline detail row rule matched a test file; test files are now excluded from it.
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