mirror of
https://github.com/we-promise/sure.git
synced 2026-06-08 04:09:04 +00:00
* feat(ds): DS::SegmentedControl; fix invisible dark selected pill (#2137) Ships DS::SegmentedControl — a single-select pill group (filters, mode switches) — plus a Lookbook preview, tests, and an exemplar migration of the budget filter tabs. The audit flagged the dark selected pill as invisible: the bespoke controls paired a container-inset track (gray-800) with a gray-700 active pill, barely a step apart. The primitive uses the DS tab-token values (tab-bg-group -> a near-black alpha-black-700 track in dark), against which the gray-700 active pill clearly reads. Values are inlined with @variant theme-dark because @apply-ing the custom tab utilities drops their dark override. - app/components/DS/segmented_control.rb: slot-based with_segment(label, active:, href:, **opts); link or button; full_width: for equal footprint; selected style isolated in .segmented-control__segment--active so a controller can toggle it as one class. - .segmented-control recipe in components.css. - Migrate budgets/_budget_tabs; budget_filter_controller now toggles the single --active class instead of five raw utility classes. Verified in-browser: dark active pill reads (gray-700 on near-black track); filter toggle still works. Tests + rubocop clean. Deferred (follow-up): auth sign-in/up switch, transaction-type tabs, and other bespoke segmented controls — same primitive, one migration each. * fix(a11y): expose segmented-control selection + derive active from filter param - DS::SegmentedControl: set aria-current (links) / aria-pressed (buttons) from the segment's active state so screen readers announce the selection. - budget_filter_controller: mirror aria-pressed when it toggles the active class. - _budget_tabs: compute each segment's initial active: from params[:filter] so a ?filter=over_budget request server-renders the correct pill (no flash before Stimulus runs). Addresses CodeRabbit reviews on #2145. * fix(ds): close unterminated segmented-control CSS rule The --active rule swallowed the table-scroll block's comment opener and never closed, so the Tailwind build died with 'Missing closing } at @layer components' and both test jobs failed at boot. Restore the closing brace and the /* opener. Also add the budgets.show.filter.aria_label locale key the budget tabs view referenced only through its inline default. --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
223 lines
7.5 KiB
CSS
223 lines
7.5 KiB
CSS
@layer components {
|
|
/* Forms */
|
|
.form-field {
|
|
@apply flex flex-col gap-1 relative px-3 py-2 rounded-md border bg-container border-secondary shadow-xs w-full;
|
|
@apply focus-within:border-secondary focus-within:shadow-none focus-within:ring-4 focus-within:ring-alpha-black-200;
|
|
@apply transition-all duration-300;
|
|
|
|
@variant theme-dark {
|
|
@apply focus-within:ring-alpha-white-300;
|
|
}
|
|
|
|
/* Add styles for multiple select within form fields */
|
|
select[multiple] {
|
|
@apply py-2 pr-2 space-y-0.5 overflow-y-auto;
|
|
|
|
option {
|
|
@apply py-2 rounded-md;
|
|
}
|
|
|
|
option:checked {
|
|
@apply after:content-['\2713'] bg-container-inset after:text-gray-500 after:ml-2;
|
|
}
|
|
|
|
option:active,
|
|
option:focus {
|
|
@apply bg-container-inset;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* New form field structure components */
|
|
.form-field__header {
|
|
@apply flex items-center justify-between gap-2;
|
|
}
|
|
|
|
.form-field__body {
|
|
@apply flex flex-col gap-1;
|
|
}
|
|
|
|
.form-field__actions {
|
|
@apply flex items-center gap-1;
|
|
}
|
|
|
|
.form-field__label {
|
|
@apply block text-xs text-secondary peer-disabled:text-subdued;
|
|
}
|
|
|
|
.form-field__input {
|
|
@apply text-primary border-none bg-container text-sm opacity-100 w-full p-0;
|
|
@apply focus:opacity-100 focus:outline-hidden focus:ring-0;
|
|
@apply placeholder-shown:opacity-50;
|
|
@apply disabled:text-subdued;
|
|
@apply text-ellipsis overflow-hidden whitespace-nowrap;
|
|
@apply transition-opacity duration-300;
|
|
@apply placeholder:text-subdued;
|
|
|
|
@variant theme-dark {
|
|
&::-webkit-calendar-picker-indicator {
|
|
filter: invert(1);
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
}
|
|
|
|
textarea.form-field__input {
|
|
@apply whitespace-normal overflow-auto;
|
|
text-overflow: clip;
|
|
}
|
|
|
|
.form-field__input--multiselect-trigger {
|
|
@apply whitespace-normal overflow-visible;
|
|
text-overflow: clip;
|
|
}
|
|
|
|
select.form-field__input,
|
|
button.form-field__input {
|
|
@apply pr-10 appearance-none;
|
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
|
background-position: right -0.15rem center;
|
|
background-repeat: no-repeat;
|
|
background-size: 1.25rem 1.25rem;
|
|
text-align: left;
|
|
}
|
|
|
|
.form-field__radio {
|
|
@apply text-primary;
|
|
}
|
|
|
|
.form-field__submit {
|
|
@apply cursor-pointer rounded-lg bg-surface p-3 text-center text-white hover:bg-surface-hover;
|
|
}
|
|
|
|
/* Checkboxes */
|
|
.checkbox {
|
|
&[type='checkbox'] {
|
|
@apply rounded-sm;
|
|
@apply transition-colors duration-300;
|
|
}
|
|
}
|
|
|
|
.checkbox--light {
|
|
&[type='checkbox'] {
|
|
@apply border-alpha-black-200 checked:bg-gray-900 checked:ring-gray-900 focus:ring-gray-900 focus-visible:ring-gray-900 checked:hover:bg-gray-300 hover:bg-gray-300;
|
|
}
|
|
|
|
&[type='checkbox']:disabled {
|
|
@apply cursor-not-allowed opacity-80 bg-gray-50 border-gray-200 checked:bg-gray-400 checked:ring-gray-400;
|
|
}
|
|
|
|
@variant theme-dark {
|
|
&[type='checkbox'] {
|
|
@apply ring-gray-900 border-alpha-white-300;
|
|
background-color: transparent;
|
|
}
|
|
|
|
&[type='checkbox']:disabled {
|
|
@apply cursor-not-allowed opacity-80 border-transparent;
|
|
background-color: var(--color-gray-700);
|
|
}
|
|
|
|
&[type='checkbox']:checked,
|
|
&[type='checkbox']:indeterminate {
|
|
@apply border-transparent;
|
|
background-color: var(--color-gray-100);
|
|
}
|
|
|
|
&[type='checkbox']:checked {
|
|
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='%23171717' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
|
|
}
|
|
|
|
&[type='checkbox']:indeterminate {
|
|
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='%23171717' xmlns='http://www.w3.org/2000/svg'%3e%3crect x='3.5' y='7' width='9' height='2' rx='1'/%3e%3c/svg%3e");
|
|
}
|
|
}
|
|
}
|
|
|
|
.checkbox--dark {
|
|
&[type='checkbox'] {
|
|
@apply ring-gray-900 checked:text-white;
|
|
}
|
|
|
|
&[type='checkbox']:disabled {
|
|
@apply cursor-not-allowed opacity-80 ring-gray-600;
|
|
}
|
|
|
|
&[type='checkbox']:checked {
|
|
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='%23111827' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
|
|
}
|
|
}
|
|
|
|
/* Tooltips */
|
|
.tooltip {
|
|
@apply hidden absolute;
|
|
}
|
|
|
|
.qrcode svg path {
|
|
fill: var(--color-black);
|
|
@variant theme-dark {
|
|
fill: var(--color-white);
|
|
}
|
|
}
|
|
|
|
/*
|
|
Segmented control (#2137) — track + equal-footprint segments. The selected
|
|
state is a single `--active` class so an optional Stimulus controller can
|
|
toggle it as one unit. Values mirror the DS tab tokens (tab-bg-group /
|
|
tab-item-active / tab-item-hover) but are inlined with `@variant theme-dark`
|
|
because `@apply`-ing a custom utility drops its dark override. The near-black
|
|
dark track (alpha-black-700) is what makes the gray-700 selected pill read —
|
|
the bespoke controls used a too-light `container-inset` track, hence the
|
|
audit's "selected pill invisible on dark." Focus = neutral outline (matches
|
|
the canonical focus ring; inlined until that token lands on main).
|
|
*/
|
|
.segmented-control {
|
|
@apply inline-flex items-center gap-0.5 p-1 rounded-lg bg-gray-50;
|
|
@variant theme-dark {
|
|
@apply bg-alpha-black-700;
|
|
}
|
|
}
|
|
|
|
.segmented-control__segment {
|
|
@apply inline-flex items-center justify-center px-2 py-1 rounded-md whitespace-nowrap;
|
|
@apply text-sm font-medium text-secondary cursor-pointer transition-colors duration-200;
|
|
@apply hover:bg-gray-200;
|
|
@apply focus-visible:outline-2 focus-visible:outline-offset-2;
|
|
@apply focus-visible:outline-alpha-black-400 theme-dark:focus-visible:outline-alpha-white-400;
|
|
@variant theme-dark {
|
|
@apply hover:bg-gray-800;
|
|
}
|
|
}
|
|
|
|
.segmented-control__segment--active {
|
|
@apply bg-white text-primary shadow-sm hover:bg-white;
|
|
@variant theme-dark {
|
|
@apply bg-gray-700 hover:bg-gray-700;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Horizontally scrollable table wrapper (#2137). `overflow-x: auto` so wide
|
|
tables scroll instead of clipping (the LLM-usage table was `overflow-hidden`
|
|
and pushed columns off-screen) or wrapping money mid-digit. The pure-CSS
|
|
"scroll shadow" gives the missing affordance: the cover gradients
|
|
(`--table-scroll-bg`, default container-inset) scroll WITH the content
|
|
(`background-attachment: local`) and hide the fixed shadow gradients at the
|
|
edges, so a soft edge-shadow only appears when there is more to scroll.
|
|
Theme-aware via `--color-shadow`. Set `--table-scroll-bg` to match the wrapper.
|
|
*/
|
|
.table-scroll {
|
|
--table-scroll-bg: var(--color-container-inset);
|
|
overflow-x: auto;
|
|
background:
|
|
linear-gradient(to right, var(--table-scroll-bg) 30%, transparent),
|
|
linear-gradient(to left, var(--table-scroll-bg) 30%, transparent) 100% 0,
|
|
radial-gradient(farthest-side at 0 50%, var(--color-shadow), transparent),
|
|
radial-gradient(farthest-side at 100% 50%, var(--color-shadow), transparent) 100% 0;
|
|
background-repeat: no-repeat;
|
|
background-color: var(--table-scroll-bg);
|
|
background-size: 32px 100%, 32px 100%, 12px 100%, 12px 100%;
|
|
background-attachment: local, local, scroll, scroll;
|
|
}
|
|
}
|