@rojaostudio/ds 2.0.0-next.27 → 2.0.0-next.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,60 +1,50 @@
1
1
  import * as react from 'react';
2
2
  import { HTMLAttributes, ReactNode } from 'react';
3
+ import { BottomBarPlacement } from './internal/bottom-bar.js';
3
4
 
4
- type FormActionsLayout = 'inline' | 'stacked' | 'bar';
5
+ type FormActionsPlacement = BottomBarPlacement;
5
6
  interface FormActionsProps extends HTMLAttributes<HTMLDivElement> {
6
7
  /**
7
- * The buttons (Figma: slot `actions`, up to 3), always in reading order: Cancelar first, the primary
8
- * last. In `stacked` the primary goes on top, and it is moved first in the page too, so the focus
9
- * order follows what is seen. In `bar`, compact (below 1024), only the last one (the primary) is shown.
8
+ * The actions (Figma: the exposed `cancel` and `primary` Buttons), in reading order: Cancelar (tone neutral, ghost)
9
+ * first, the primary (tone action, fill) last. Expanded (from 1024) they all show, on the right; compact (below
10
+ * 1024) only the last one, the primary: the others are `display: none`, and the way out is the topbar's X.
10
11
  */
11
12
  children: ReactNode;
12
13
  /**
13
- * A short supporting sentence beside the actions (Figma: `showHelper` + `helper`). It is a live region
14
- * (`role="status"`): when it changes (say, "Falta preço e prazo"), it is said without moving focus.
14
+ * docked (default: the container's width, against the foot) or floating (from 1024 up: off the foot, rounded, the
15
+ * overlay shadow, 24 of margin, at most 768, centred). Below 1024 floating is docked (Figma: `placement`).
15
16
  */
16
- helper?: ReactNode;
17
+ placement?: FormActionsPlacement;
17
18
  /**
18
- * inline (desktop: floats and hugs its content, sentence left, actions right), stacked (phone, a task
19
- * with no way out in the topbar: edge to edge at the foot, primary full width on top) or bar (the foot of
20
- * a create form: the SavingBar's skeleton, neutral; expanded from 1024, compact in one row below it)
21
- * (Figma: `layout`).
19
+ * One ~44 control on the left, never running text (Figma: slot `leading`, `showLeading`). Present, even `null`
20
+ * while its control is hidden, it reserves 44 so the actions do not jump. Whoever fills it says its own changes
21
+ * (its own aria-live); the bar says nothing.
22
22
  */
23
- layout?: FormActionsLayout;
24
- /**
25
- * `bar` only, with a `helper`: turns the helper into a button that opens what is missing (a Drawer with
26
- * the checklist), in the compact arrangement only; expanded, the helper stays text (Figma: `showDetails`).
27
- */
28
- onDetails?: () => void;
29
- /** Said after the helper in the details button's accessible name, hidden on screen. */
30
- detailsLabel?: string;
31
- /** Whether the panel the details button opens is open (aria-expanded). */
32
- detailsExpanded?: boolean;
33
- /** The id of that panel (aria-controls). */
34
- detailsControls?: string;
23
+ leading?: ReactNode;
35
24
  }
36
25
  /**
37
- * FormActions — Figma [RDS] Actions/FormActions. The actions at the end of a create form or a task,
38
- * there from the first second. Where it sticks is the page's job: `position: sticky` at the bottom of
39
- * the scrolling container. Styles: form-actions.css.
26
+ * FormActions — Figma [RDS] Actions/FormActions. The actions at the end of a create form or of a long task (Cancelar
27
+ * and the primary), at the foot of the screen from the first second. Only actions: what is missing is the project's
28
+ * (if it needs a control, it goes in `leading`). Built on the private BottomBar (.rds-bottom-bar), the SavingBar's
29
+ * shell: a band in the brand's primary colour (bottom-bar/background), 64 plus the safe area compact, 68 expanded,
30
+ * 24 at the sides, sticky at the bottom of the scrolling container. Styles: form-actions.css, internal/bottom-bar.css.
40
31
  *
41
- * `layout="bar"` is the foot of a create form: the SavingBar's skeleton (68 expanded, 64 plus the safe area
42
- * compact, 24 at the sides, the primary in the same place), but neutral: form-actions/bar/background
43
- * (surface/page) with the hairline form-actions/border on top, never the brand's colour (that is the
44
- * SavingBar's signal, for a pending edit). Below 1024 (the screen width, as the SavingBar) it is one row:
45
- * the helper on the left (at most 2 lines; with `onDetails`, the button that opens what is missing) and only
46
- * the primary on the right.
32
+ * `<FormActions placement="docked" leading={…}><Button tone="neutral" variant="ghost">Cancelar</Button><Button
33
+ * tone="action">Criar produto</Button></FormActions>`: the last child is the primary; compact, the others leave.
47
34
  *
48
35
  * The app's rules around it:
49
- * - The primary is never disabled: validate on click, show the error on the field and move the focus to the
50
- * first pending one.
51
- * - The scrolling container takes `scroll-padding-bottom` equal to the bar's height plus
52
- * `env(safe-area-inset-bottom)`, so a focused field is never hidden behind it (WCAG 2.4.11).
53
- * - On the phone the bar leaves while the on-screen keyboard is open (it would cover the field being typed).
54
- * - Compact has no Cancelar: the way out is back or X in the topbar, with an AlertDialog "Sair sem criar?"
55
- * ([Continuar editando] [Sair sem criar]) when something was typed. Cancelar can also go in the footer of
56
- * the details Drawer.
36
+ * - The primary is never disabled: validate on click, show the error on the field and move the focus to the first
37
+ * pending one.
38
+ * - The scrolling container takes `scroll-padding-bottom` (and the content `padding-bottom`) equal to the bar's height
39
+ * (plus the margin when floating) plus `env(safe-area-inset-bottom)`, so a focused field is never hidden behind it
40
+ * (WCAG 2.4.11).
41
+ * - On the phone (below 1024) the bar leaves while the on-screen keyboard is open (a field is focused and the visible
42
+ * area shrinks): it slides down (no motion with prefers-reduced-motion) and comes back on blur; it never takes the
43
+ * focus.
44
+ * - Compact has no Cancelar: the way out is the X in the topbar, with an AlertDialog "Sair sem criar?" ([Continuar
45
+ * editando] [Sair sem criar]) when something was typed.
46
+ * - Buttons at the end of a card or a dialog are loose Buttons or a ButtonGroup, not this.
57
47
  */
58
- declare function FormActions({ children, helper, layout, onDetails, detailsLabel, detailsExpanded, detailsControls, className, ...rest }: FormActionsProps): react.JSX.Element;
48
+ declare function FormActions({ children, placement, leading, className, ...rest }: FormActionsProps): react.JSX.Element;
59
49
 
60
- export { FormActions, type FormActionsLayout, type FormActionsProps };
50
+ export { FormActions, type FormActionsPlacement, type FormActionsProps };
@@ -1,71 +1,14 @@
1
+ "use client";
1
2
  import {
2
3
  __objRest,
3
4
  __spreadProps,
4
5
  __spreadValues
5
6
  } from "../chunk-ORMEWXMH.js";
6
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
- import { Children } from "react";
8
- import { ChevronUpIcon } from "./internal/icons.js";
7
+ import { jsx } from "react/jsx-runtime";
8
+ import { BottomBar } from "./internal/bottom-bar.js";
9
9
  function FormActions(_a) {
10
- var _b = _a, {
11
- children,
12
- helper,
13
- layout = "inline",
14
- onDetails,
15
- detailsLabel = "Ver o que falta",
16
- detailsExpanded,
17
- detailsControls,
18
- className
19
- } = _b, rest = __objRest(_b, [
20
- "children",
21
- "helper",
22
- "layout",
23
- "onDetails",
24
- "detailsLabel",
25
- "detailsExpanded",
26
- "detailsControls",
27
- "className"
28
- ]);
29
- const actions = Children.toArray(children);
30
- const bar = layout === "bar";
31
- const details = bar && helper != null && helper !== false && onDetails !== void 0;
32
- return /* @__PURE__ */ jsxs(
33
- "div",
34
- __spreadProps(__spreadValues({}, rest), {
35
- className: ["rds-form-actions", `rds-form-actions--${layout}`, className].filter(Boolean).join(" "),
36
- children: [
37
- helper && /* @__PURE__ */ jsx(
38
- "p",
39
- {
40
- className: ["rds-form-actions__helper", details && "rds-form-actions__helper--details"].filter(Boolean).join(" "),
41
- role: "status",
42
- children: bar ? /* @__PURE__ */ jsxs(Fragment, { children: [
43
- /* @__PURE__ */ jsx("span", { className: "rds-form-actions__text", children: helper }),
44
- details && /* @__PURE__ */ jsxs(
45
- "button",
46
- {
47
- type: "button",
48
- className: "rds-form-actions__details",
49
- "aria-expanded": detailsExpanded != null ? detailsExpanded : false,
50
- "aria-controls": detailsControls,
51
- onClick: onDetails,
52
- children: [
53
- /* @__PURE__ */ jsx("span", { className: "rds-form-actions__text", children: helper }),
54
- /* @__PURE__ */ jsxs("span", { className: "rds-visually-hidden", children: [
55
- ", ",
56
- detailsLabel
57
- ] }),
58
- /* @__PURE__ */ jsx("span", { className: "rds-form-actions__icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx(ChevronUpIcon, {}) })
59
- ]
60
- }
61
- )
62
- ] }) : helper
63
- }
64
- ),
65
- /* @__PURE__ */ jsx("div", { className: "rds-form-actions__actions", children: layout === "stacked" ? actions.reverse() : actions })
66
- ]
67
- })
68
- );
10
+ var _b = _a, { children, placement = "docked", leading, className } = _b, rest = __objRest(_b, ["children", "placement", "leading", "className"]);
11
+ return /* @__PURE__ */ jsx(BottomBar, __spreadProps(__spreadValues({}, rest), { placement, leading, className: ["rds-form-actions", className].filter(Boolean).join(" "), children: /* @__PURE__ */ jsx("div", { className: "rds-form-actions__actions", children }) }));
69
12
  }
70
13
  export {
71
14
  FormActions
@@ -69,7 +69,7 @@ export { Tabs, TabsContent, TabsContentProps, TabsList, TabsListProps, TabsProps
69
69
  export { PageHeader, PageHeaderHeading, PageHeaderProps } from './page-header.js';
70
70
  export { Sidebar, SidebarHeader, SidebarItem, SidebarItemProps, SidebarProps, SidebarSection, SidebarSectionProps, SidebarSeparator } from './sidebar.js';
71
71
  export { NavigationMenu, NavigationMenuEntry, NavigationMenuLink, NavigationMenuLinkComponent, NavigationMenuProps } from './navigation-menu.js';
72
- export { SavingBar, SavingBarProps, SavingBarStatus } from './saving-bar.js';
72
+ export { SavingBar, SavingBarPlacement, SavingBarProps, SavingBarStatus } from './saving-bar.js';
73
73
  export { SavingBarProvider, SavingBarRegistration, SavingBarRoot, usePageSavingBar } from './saving-bar-context.js';
74
74
  export { FAB, FABConfig, FABProps, FABProvider, FABRoot, useFAB, usePageFAB } from './fab.js';
75
75
  export { Table, TableAlign, TableBody, TableCell, TableCellProps, TableHead, TableHeadProps, TableHeader, TableProps, TableRow, TableSort, TableStatus, TableStatusProps } from './table.js';
@@ -84,7 +84,7 @@ export { Avatar, AvatarContent, AvatarGroup, AvatarGroupProps, AvatarGroupSize,
84
84
  export { IconButton, IconButtonProps } from './icon-button.js';
85
85
  export { ButtonGroup, ButtonGroupOrientation, ButtonGroupProps } from './button-group.js';
86
86
  export { ActionBar, ActionBarProps } from './action-bar.js';
87
- export { FormActions, FormActionsLayout, FormActionsProps } from './form-actions.js';
87
+ export { FormActions, FormActionsPlacement, FormActionsProps } from './form-actions.js';
88
88
  export { Breadcrumb, BreadcrumbItem, BreadcrumbLinkComponent, BreadcrumbProps, BreadcrumbTone } from './breadcrumb.js';
89
89
  export { Pagination, PaginationLinkComponent, PaginationProps, pageItems } from './pagination.js';
90
90
  export { Popover, PopoverAlign, PopoverClose, PopoverProps, PopoverSide } from './popover.js';
@@ -136,6 +136,7 @@ import '@radix-ui/react-dialog';
136
136
  import './internal/modal.js';
137
137
  import './internal/menu.js';
138
138
  import './internal/fieldset.js';
139
+ import './internal/bottom-bar.js';
139
140
  import '@radix-ui/react-toggle';
140
141
  import '@radix-ui/react-tooltip';
141
142
  import '@radix-ui/react-popover';
@@ -0,0 +1,30 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes, ReactNode } from 'react';
3
+
4
+ type BottomBarPlacement = 'docked' | 'floating';
5
+ interface BottomBarProps extends HTMLAttributes<HTMLDivElement> {
6
+ /**
7
+ * docked (the container's width, against the foot, square corners) or floating (from lg 1024 up: off the foot by
8
+ * bottom-bar/floating/margin, at most bottom-bar/floating/max-width, centred, bottom-bar/floating/radius and the
9
+ * elevation/overlay shadow). Below 1024 floating is exactly docked (Figma: `placement`).
10
+ */
11
+ placement?: BottomBarPlacement;
12
+ /**
13
+ * The leading slot, on the left: one ~44 control, never running text. Present (even `null` or `false` while its
14
+ * control is hidden), the slot is there and reserves bottom-bar/leading/min-width, so what follows does not jump
15
+ * when the control comes and goes; left out (`undefined`), there is no slot. Whoever fills it says its own changes.
16
+ */
17
+ leading?: ReactNode;
18
+ children: ReactNode;
19
+ }
20
+ /**
21
+ * BottomBar — Figma [RDS] Actions/.bottom-bar, private: the shell at the foot of the screen under the FormActions and
22
+ * the SavingBar. Not exported from the package. It owns the colour (bottom-bar/background, the brand's primary), the
23
+ * height (64 plus env(safe-area-inset-bottom) compact, 68 expanded), the padding and gap, the place (sticky at the
24
+ * bottom of the scrolling container), the placement, the leading slot, the Button tokens in the bar's colours and the
25
+ * on-screen keyboard (compact: it slides away while a field is being typed in). Whoever builds on it puts the
26
+ * actions. Styles: bottom-bar.css.
27
+ */
28
+ declare function BottomBar({ placement, leading, className, children, ...rest }: BottomBarProps): react.JSX.Element;
29
+
30
+ export { BottomBar, type BottomBarPlacement, type BottomBarProps };
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import {
3
+ __objRest,
4
+ __spreadProps,
5
+ __spreadValues
6
+ } from "../../chunk-ORMEWXMH.js";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { useOnScreenKeyboard } from "./use-on-screen-keyboard.js";
9
+ function BottomBar(_a) {
10
+ var _b = _a, { placement = "docked", leading, className, children } = _b, rest = __objRest(_b, ["placement", "leading", "className", "children"]);
11
+ const keyboard = useOnScreenKeyboard();
12
+ return /* @__PURE__ */ jsxs(
13
+ "div",
14
+ __spreadProps(__spreadValues({}, rest), {
15
+ className: ["rds-bottom-bar", `rds-bottom-bar--${placement}`, keyboard && "rds-bottom-bar--keyboard", className].filter(Boolean).join(" "),
16
+ children: [
17
+ leading !== void 0 && /* @__PURE__ */ jsx("div", { className: "rds-bottom-bar__leading", children: leading }),
18
+ children
19
+ ]
20
+ })
21
+ );
22
+ }
23
+ export {
24
+ BottomBar
25
+ };
@@ -12,8 +12,6 @@ declare function CheckIcon(): react.JSX.Element;
12
12
  declare function MinusIcon(): react.JSX.Element;
13
13
  /** A chevron pointing down: opens a list. */
14
14
  declare function ChevronDownIcon(): react.JSX.Element;
15
- /** A chevron pointing up: opens a panel that comes up from below (the SavingBar's details). */
16
- declare function ChevronUpIcon(): react.JSX.Element;
17
15
  /** An open eye: shows what is hidden (a password). */
18
16
  declare function EyeIcon(): react.JSX.Element;
19
17
  /** A crossed eye: hides again what was shown. */
@@ -115,4 +113,4 @@ declare function RectangleHorizontalIcon(): react.JSX.Element;
115
113
  /** A dashed square: a free crop, no fixed aspect. */
116
114
  declare function SquareDashedIcon(): react.JSX.Element;
117
115
 
118
- export { AlertIcon, ArrowDownIcon, ArrowLeftIcon, ArrowUpDownIcon, ArrowUpIcon, BadgeCheckIcon, BuildingIcon, CalendarIcon, CameraIcon, CheckCheckIcon, CheckIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon, CircleCheckIcon, ClockIcon, CloseIcon, CopyIcon, ExternalLinkIcon, EyeIcon, EyeOffIcon, FileTextIcon, ImageIcon, ImageOffIcon, ImagePlusIcon, InfoIcon, LockIcon, MapPinIcon, MegaphoneIcon, MenuIcon, MicIcon, MinusIcon, MoreVerticalIcon, PaperclipIcon, PencilIcon, PhoneIcon, PlusIcon, QuoteIcon, RectangleHorizontalIcon, RectangleVerticalIcon, ReplyIcon, RotateCcwIcon, SearchIcon, SearchXIcon, SlidersIcon, SmileIcon, SparklesIcon, SquareDashedIcon, SquareIcon, StarFilledIcon, StarIcon, TableIcon, ThumbsUpIcon, TriangleAlertIcon, UploadIcon, UserIcon };
116
+ export { AlertIcon, ArrowDownIcon, ArrowLeftIcon, ArrowUpDownIcon, ArrowUpIcon, BadgeCheckIcon, BuildingIcon, CalendarIcon, CameraIcon, CheckCheckIcon, CheckIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, CircleCheckIcon, ClockIcon, CloseIcon, CopyIcon, ExternalLinkIcon, EyeIcon, EyeOffIcon, FileTextIcon, ImageIcon, ImageOffIcon, ImagePlusIcon, InfoIcon, LockIcon, MapPinIcon, MegaphoneIcon, MenuIcon, MicIcon, MinusIcon, MoreVerticalIcon, PaperclipIcon, PencilIcon, PhoneIcon, PlusIcon, QuoteIcon, RectangleHorizontalIcon, RectangleVerticalIcon, ReplyIcon, RotateCcwIcon, SearchIcon, SearchXIcon, SlidersIcon, SmileIcon, SparklesIcon, SquareDashedIcon, SquareIcon, StarFilledIcon, StarIcon, TableIcon, ThumbsUpIcon, TriangleAlertIcon, UploadIcon, UserIcon };
@@ -21,9 +21,6 @@ function MinusIcon() {
21
21
  function ChevronDownIcon() {
22
22
  return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" }) });
23
23
  }
24
- function ChevronUpIcon() {
25
- return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "m18 15-6-6-6 6" }) });
26
- }
27
24
  function EyeIcon() {
28
25
  return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
29
26
  /* @__PURE__ */ jsx("path", { d: "M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" }),
@@ -290,7 +287,6 @@ export {
290
287
  ChevronDownIcon,
291
288
  ChevronLeftIcon,
292
289
  ChevronRightIcon,
293
- ChevronUpIcon,
294
290
  CircleCheckIcon,
295
291
  ClockIcon,
296
292
  CloseIcon,
@@ -0,0 +1,12 @@
1
+ /** Whether focusing this element brings up the on-screen keyboard: a text field, a text area or contenteditable. */
2
+ declare function isEditable(el: Element | null): boolean;
3
+ /**
4
+ * Whether the phone's on-screen keyboard is open: compact screen (below 1024), an editable field focused, and the
5
+ * visible area (visualViewport, scaled back from a pinch zoom) well short of the layout viewport. For the bars at the
6
+ * foot of the screen (FormActions, SavingBar), which leave while it is open so they do not cover the field being
7
+ * typed in. Without visualViewport (old browsers) or on the server it is always false: the bar stays.
8
+ * `enabled` false skips the listeners (a layout that does not sit at the foot).
9
+ */
10
+ declare function useOnScreenKeyboard(enabled?: boolean): boolean;
11
+
12
+ export { isEditable, useOnScreenKeyboard };
@@ -0,0 +1,49 @@
1
+ "use client";
2
+ import "../../chunk-ORMEWXMH.js";
3
+ import { useEffect, useState } from "react";
4
+ const COMPACT = "(max-width: 1023px)";
5
+ const KEYBOARD_RATIO = 0.75;
6
+ const NO_KEYBOARD = /* @__PURE__ */ new Set(["button", "checkbox", "color", "file", "hidden", "image", "radio", "range", "reset", "submit"]);
7
+ function isEditable(el) {
8
+ if (!(el instanceof HTMLElement)) return false;
9
+ if (el.isContentEditable) return true;
10
+ if (el instanceof HTMLTextAreaElement) return !el.readOnly && !el.disabled;
11
+ if (el instanceof HTMLInputElement) return !NO_KEYBOARD.has(el.type) && !el.readOnly && !el.disabled;
12
+ return false;
13
+ }
14
+ function useOnScreenKeyboard(enabled = true) {
15
+ const [open, setOpen] = useState(false);
16
+ useEffect(() => {
17
+ const viewport = window.visualViewport;
18
+ if (!enabled || !viewport || typeof window.matchMedia !== "function") {
19
+ setOpen(false);
20
+ return;
21
+ }
22
+ const compact = window.matchMedia(COMPACT);
23
+ let frame = 0;
24
+ const measure = () => compact.matches && isEditable(document.activeElement) && viewport.height * viewport.scale < window.innerHeight * KEYBOARD_RATIO;
25
+ const check = () => {
26
+ cancelAnimationFrame(frame);
27
+ frame = requestAnimationFrame(() => setOpen(measure()));
28
+ };
29
+ check();
30
+ viewport.addEventListener("resize", check);
31
+ window.addEventListener("resize", check);
32
+ document.addEventListener("focusin", check);
33
+ document.addEventListener("focusout", check);
34
+ compact.addEventListener("change", check);
35
+ return () => {
36
+ cancelAnimationFrame(frame);
37
+ viewport.removeEventListener("resize", check);
38
+ window.removeEventListener("resize", check);
39
+ document.removeEventListener("focusin", check);
40
+ document.removeEventListener("focusout", check);
41
+ compact.removeEventListener("change", check);
42
+ };
43
+ }, [enabled]);
44
+ return enabled && open;
45
+ }
46
+ export {
47
+ isEditable,
48
+ useOnScreenKeyboard
49
+ };
@@ -1,7 +1,9 @@
1
1
  import * as react from 'react';
2
2
  import { HTMLAttributes, ReactNode } from 'react';
3
+ import { BottomBarPlacement } from './internal/bottom-bar.js';
3
4
 
4
5
  type SavingBarStatus = 'unsaved' | 'saving' | 'error';
6
+ type SavingBarPlacement = BottomBarPlacement;
5
7
  interface SavingBarProps extends HTMLAttributes<HTMLDivElement> {
6
8
  /**
7
9
  * unsaved: the message and the actions; saving: `savingMessage`, Salvar in loading (`savingLabel`, the only
@@ -9,7 +11,7 @@ interface SavingBarProps extends HTMLAttributes<HTMLDivElement> {
9
11
  * becomes `retryLabel` (Figma: `status`).
10
12
  */
11
13
  status?: SavingBarStatus;
12
- /** The unsaved text (Figma: `message`). */
14
+ /** The unsaved text, at most 2 lines with an ellipsis (Figma: `message`). */
13
15
  message?: ReactNode;
14
16
  /** The text while saving. */
15
17
  savingMessage?: ReactNode;
@@ -21,35 +23,37 @@ interface SavingBarProps extends HTMLAttributes<HTMLDivElement> {
21
23
  onSave: () => void;
22
24
  /**
23
25
  * Shows Descartar and is called when it is pressed (Figma: `showDiscard`). Expanded only: in the compact
24
- * arrangement (the phone) the bar has only the main button, and Descartar belongs to the app, in the footer of
25
- * the details Drawer or in the confirmation when leaving with changes (AlertDialog).
26
+ * arrangement (the phone) the bar has only the main button, and Descartar belongs to the way out, in the
27
+ * confirmation when leaving with changes (AlertDialog).
26
28
  */
27
29
  onDiscard?: () => void;
28
30
  saveLabel?: string;
29
31
  retryLabel?: string;
30
32
  discardLabel?: string;
31
33
  /**
32
- * Turns the unsaved message into a button that opens what is missing (a Drawer with the checklist), in the
33
- * compact arrangement only; expanded, the message stays text (Figma: `showDetails`).
34
+ * docked (default: the container's width, against the foot) or floating (from 1024 up: off the foot, rounded, the
35
+ * overlay shadow, 24 of margin, at most 768, centred). Below 1024 floating is docked (Figma: `placement`).
34
36
  */
35
- onDetails?: () => void;
36
- /** Said after the message in the details button's accessible name, hidden on screen. */
37
- detailsLabel?: string;
38
- /** Whether the panel the details button opens is open (aria-expanded). */
39
- detailsExpanded?: boolean;
40
- /** The id of that panel (aria-controls). */
41
- detailsControls?: string;
37
+ placement?: SavingBarPlacement;
38
+ /**
39
+ * One ~44 control on the left of the message, never running text (Figma: slot `leading`, `showLeading`). Present,
40
+ * even `null` while its control is hidden, it reserves 44 so the message and the buttons do not jump. Whoever fills
41
+ * it says its own changes (its own aria-live); the bar does not repeat them.
42
+ */
43
+ leading?: ReactNode;
42
44
  }
43
45
  /**
44
- * SavingBar — Figma [RDS] Actions/SavingBar. Shows at the foot of the screen while a change is not
45
- * saved, and leaves once saved. It is the brand's primary colour (savingbar/background, colors/primary/default),
46
- * in light and dark and in every brand: a state that waits for a decision, not a notice. It does not place itself:
47
- * put it sticky or fixed at the bottom. Its Buttons are Salvar in tone action and Descartar in tone neutral ghost,
48
- * with the bar's colours (savingbar/button/*). Below 1024 (compact, the phone) it is one row, 64 tall plus the
49
- * safe area: the message (or the details button) on the left and only Salvar on the right; Descartar is the app's
50
- * there, in the footer of the details Drawer or in the confirmation when leaving. While saving, the indicator is
51
- * Salvar in loading, alone (with the LoadingOverlay too). Styles: saving-bar.css.
46
+ * SavingBar — Figma [RDS] Actions/SavingBar. Shows at the foot of the screen while a change is not saved, and leaves
47
+ * once saved (a Toast confirms). Built on the private BottomBar (.rds-bottom-bar), the FormActions' shell: the brand's
48
+ * primary colour (bottom-bar/background) in light, dark and every brand, 64 plus the safe area compact, 68 expanded,
49
+ * 24 at the sides, sticky at the bottom of the scrolling container. Its Buttons are Salvar in tone action fill and
50
+ * Descartar in tone neutral ghost, with the bar's colours (bottom-bar/button/*). Below 1024 (compact, the phone) it is
51
+ * one row: the message on the left and only Salvar on the right; Descartar is in the confirmation when leaving. While
52
+ * saving, the indicator is Salvar in loading, alone (with the LoadingOverlay too). On the phone it leaves while the
53
+ * on-screen keyboard is open and comes back on blur, never taking the focus. The content needs room below as tall as
54
+ * the bar (`scroll-padding-bottom`, plus `env(safe-area-inset-bottom)`). Styles: saving-bar.css,
55
+ * internal/bottom-bar.css.
52
56
  */
53
- declare function SavingBar({ status, message, savingMessage, savingLabel, errorMessage, onSave, onDiscard, saveLabel, retryLabel, discardLabel, onDetails, detailsLabel, detailsExpanded, detailsControls, className, ...rest }: SavingBarProps): react.JSX.Element;
57
+ declare function SavingBar({ status, message, savingMessage, savingLabel, errorMessage, onSave, onDiscard, saveLabel, retryLabel, discardLabel, placement, leading, className, ...rest }: SavingBarProps): react.JSX.Element;
54
58
 
55
- export { SavingBar, type SavingBarProps, type SavingBarStatus };
59
+ export { SavingBar, type SavingBarPlacement, type SavingBarProps, type SavingBarStatus };
@@ -6,7 +6,8 @@ import {
6
6
  } from "../chunk-ORMEWXMH.js";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  import { Button } from "./button.js";
9
- import { AlertIcon, ChevronUpIcon } from "./internal/icons.js";
9
+ import { BottomBar } from "./internal/bottom-bar.js";
10
+ import { AlertIcon } from "./internal/icons.js";
10
11
  function SavingBar(_a) {
11
12
  var _b = _a, {
12
13
  status = "unsaved",
@@ -19,10 +20,8 @@ function SavingBar(_a) {
19
20
  saveLabel = "Salvar",
20
21
  retryLabel = "Tentar de novo",
21
22
  discardLabel = "Descartar",
22
- onDetails,
23
- detailsLabel = "Ver o que falta",
24
- detailsExpanded,
25
- detailsControls,
23
+ placement = "docked",
24
+ leading,
26
25
  className
27
26
  } = _b, rest = __objRest(_b, [
28
27
  "status",
@@ -35,51 +34,32 @@ function SavingBar(_a) {
35
34
  "saveLabel",
36
35
  "retryLabel",
37
36
  "discardLabel",
38
- "onDetails",
39
- "detailsLabel",
40
- "detailsExpanded",
41
- "detailsControls",
37
+ "placement",
38
+ "leading",
42
39
  "className"
43
40
  ]);
44
41
  const saving = status === "saving";
45
42
  const error = status === "error";
46
43
  const text = saving ? savingMessage : error ? errorMessage : message;
47
- const details = status === "unsaved" && onDetails !== void 0;
48
- return /* @__PURE__ */ jsx(
49
- "div",
44
+ return /* @__PURE__ */ jsxs(
45
+ BottomBar,
50
46
  __spreadProps(__spreadValues({
51
47
  role: "region",
52
48
  "aria-label": "Salvar altera\xE7\xF5es"
53
49
  }, rest), {
50
+ placement,
51
+ leading,
54
52
  className: ["rds-savingbar", className].filter(Boolean).join(" "),
55
- children: /* @__PURE__ */ jsxs("div", { className: "rds-savingbar__row", children: [
56
- /* @__PURE__ */ jsxs("p", { className: ["rds-savingbar__message", details && "rds-savingbar__message--details"].filter(Boolean).join(" "), role: "status", children: [
53
+ children: [
54
+ /* @__PURE__ */ jsxs("p", { className: "rds-savingbar__message", role: "status", children: [
57
55
  error && /* @__PURE__ */ jsx("span", { className: "rds-savingbar__icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx(AlertIcon, {}) }),
58
- /* @__PURE__ */ jsx("span", { className: "rds-savingbar__text", children: text }),
59
- details && /* @__PURE__ */ jsxs(
60
- "button",
61
- {
62
- type: "button",
63
- className: "rds-savingbar__details",
64
- "aria-expanded": detailsExpanded != null ? detailsExpanded : false,
65
- "aria-controls": detailsControls,
66
- onClick: onDetails,
67
- children: [
68
- /* @__PURE__ */ jsx("span", { className: "rds-savingbar__text", children: text }),
69
- /* @__PURE__ */ jsxs("span", { className: "rds-visually-hidden", children: [
70
- ", ",
71
- detailsLabel
72
- ] }),
73
- /* @__PURE__ */ jsx("span", { className: "rds-savingbar__icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx(ChevronUpIcon, {}) })
74
- ]
75
- }
76
- )
56
+ /* @__PURE__ */ jsx("span", { className: "rds-savingbar__text", children: text })
77
57
  ] }),
78
58
  /* @__PURE__ */ jsxs("div", { className: "rds-savingbar__actions", children: [
79
59
  onDiscard && /* @__PURE__ */ jsx(Button, { className: "rds-savingbar__discard", tone: "neutral", variant: "ghost", disabled: saving, onClick: onDiscard, children: discardLabel }),
80
60
  /* @__PURE__ */ jsx(Button, { tone: "action", variant: "fill", loading: saving, loadingLabel: savingLabel, onClick: onSave, children: saving ? savingLabel : error ? retryLabel : saveLabel })
81
61
  ] })
82
- ] })
62
+ ]
83
63
  })
84
64
  );
85
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rojaostudio/ds",
3
- "version": "2.0.0-next.27",
3
+ "version": "2.0.0-next.30",
4
4
  "type": "module",
5
5
  "description": "Rojão Design System — componentes React, estilos e alvo React Native. O motor vive em @rojaostudio/ds-core.",
6
6
  "license": "MIT",
@@ -4650,195 +4650,24 @@ input:focus-visible + .rds-checkbox__box {
4650
4650
  }
4651
4651
 
4652
4652
  /* components/form-actions.css */
4653
- /* @tokens form-actions */
4653
+ /* @tokens bottom-bar */
4654
4654
  /*
4655
- * FormActions (Actions collection, form-actions/*): the actions at the end of a form, on the card
4656
- * surface with a hairline. inline floats (container radius, raised elevation, 16 24 padding, 24 between
4657
- * sentence and actions); stacked sticks to the foot edge to edge (16 padding, hairline on top).
4658
- * bar is the foot of a create form, the SavingBar's skeleton but neutral: form-actions/bar/background
4659
- * (surface/page) with the hairline form-actions/border on top (drawn inside, so it adds no height), full
4660
- * width (at least 320), 16 between the helper and the actions (8 apart). Two arrangements by the screen
4661
- * width, as the SavingBar (layout/compact below lg 1024): expanded, 12 24 padding (68 = 12 + 44 + 12), the
4662
- * helper grows (at least 160) beside Cancelar and the primary; compact, one row, 10 24 padding
4663
- * (64 = 10 + 44 + 10) plus the safe area at the bottom, the helper (flex 1, at most 2 lines) on the left and
4664
- * only the last action (the primary) on the right.
4665
- * Details (showDetails, onDetails in code): compact, the helper becomes a text button with chevron-up 20 at
4666
- * the right (12 apart); hover underlines, focus draws form-actions/focus/ring (focus/ring-width, radius/focus);
4667
- * the touch area is 44 tall without changing the drawing. Expanded, the helper stays text.
4655
+ * FormActions: no tokens of its own; the shell (internal/bottom-bar.css, bottom-bar/*) brings the colour, the height,
4656
+ * the padding, the place and the Buttons' colours. Here only the actions: on the right, 8 apart, Cancelar then the
4657
+ * primary. Compact (below lg 1024, by the screen width as the Figma viewport mode), only the last one, the primary.
4668
4658
  */
4669
- .rds-form-actions {
4670
- box-sizing: border-box;
4671
- display: flex;
4672
- align-items: center;
4673
- background: var(--form-actions-background);
4674
- color: var(--form-actions-text);
4675
- font-family: var(--type-font-stack);
4676
- }
4677
-
4678
- .rds-form-actions__helper {
4679
- flex: 1;
4680
- min-width: 0;
4681
- margin: 0;
4682
- font-size: var(--type-label-size);
4683
- line-height: var(--type-label-line);
4684
- }
4685
-
4686
4659
  .rds-form-actions__actions {
4687
4660
  display: flex;
4688
- flex-wrap: wrap;
4689
- gap: var(--space-8);
4690
- }
4691
-
4692
- /* inline: floats and hugs its content; the page decides where. */
4693
- .rds-form-actions--inline {
4694
- display: inline-flex;
4695
- flex-wrap: wrap;
4696
- justify-content: flex-end;
4697
- gap: var(--space-24);
4698
- max-width: 100%;
4699
- padding: var(--space-16) var(--space-24);
4700
- border: var(--border-width) solid var(--form-actions-border);
4701
- border-radius: var(--radius-container);
4702
- box-shadow: var(--elevation-raised);
4703
- }
4704
-
4705
- /* stacked: edge to edge at the foot, the buttons full width. */
4706
- .rds-form-actions--stacked {
4707
- flex-direction: column;
4708
- align-items: stretch;
4709
- gap: var(--space-8);
4710
- width: 100%;
4711
- padding: var(--space-16);
4712
- border-top: var(--border-width) solid var(--form-actions-border);
4713
- }
4714
-
4715
- .rds-form-actions--stacked .rds-form-actions__actions {
4716
- flex-direction: column;
4717
- }
4718
-
4719
- .rds-form-actions--stacked .rds-form-actions__actions > * {
4720
- width: 100%;
4721
- }
4722
-
4723
- /* bar: the foot of a create form, the SavingBar's skeleton, neutral. */
4724
- .rds-form-actions--bar {
4725
- gap: var(--space-16);
4726
- justify-content: flex-end;
4727
- width: 100%;
4728
- min-width: min(320px, 100%);
4729
- padding: var(--space-12) var(--space-24);
4730
- background: var(--form-actions-bar-background);
4731
- /* The hairline on top, inside: the bar stays 68 / 64 tall. */
4732
- box-shadow: inset 0 var(--border-width) 0 var(--form-actions-border);
4733
- }
4734
-
4735
- .rds-form-actions--bar .rds-form-actions__helper {
4736
- display: flex;
4737
- flex: 1 1 160px;
4738
- align-items: center;
4739
- min-width: 160px;
4740
- }
4741
-
4742
- .rds-form-actions--bar .rds-form-actions__actions {
4743
- flex: none;
4744
- flex-wrap: nowrap;
4745
- }
4746
-
4747
- .rds-form-actions__text {
4748
- display: -webkit-box;
4749
- flex: 1;
4750
- min-width: 0;
4751
- overflow: hidden;
4752
- -webkit-box-orient: vertical;
4753
- -webkit-line-clamp: 2;
4754
- line-clamp: 2;
4755
- }
4756
-
4757
- .rds-form-actions__icon {
4758
- display: inline-flex;
4759
4661
  flex: none;
4760
- width: var(--size-20);
4761
- height: var(--size-20);
4762
- }
4763
-
4764
- .rds-form-actions__icon > svg {
4765
- width: 100%;
4766
- height: 100%;
4767
- }
4768
-
4769
- .rds-form-actions__details {
4770
- position: relative;
4771
- display: none;
4772
- flex: 1;
4773
- align-items: center;
4774
- gap: var(--space-12);
4775
- min-width: 0;
4776
- margin: 0;
4777
- padding: 0;
4778
- border: 0;
4779
- border-radius: var(--radius-focus);
4780
- background: none;
4781
- color: inherit;
4782
- font: inherit;
4783
- text-align: start;
4784
- cursor: pointer;
4785
- }
4786
-
4787
- /* The touch area: 44 tall around the helper, invisible. */
4788
- .rds-form-actions__details::after {
4789
- content: '';
4790
- position: absolute;
4791
- top: 50%;
4792
- right: 0;
4793
- left: 0;
4794
- min-height: 44px;
4795
- height: 100%;
4796
- transform: translateY(-50%);
4797
- }
4798
-
4799
- .rds-form-actions__details:hover .rds-form-actions__text {
4800
- text-decoration: underline;
4801
- }
4802
-
4803
- .rds-form-actions__details:focus-visible {
4804
- outline: var(--focus-ring-width) solid var(--form-actions-focus-ring);
4805
- outline-offset: var(--focus-ring-width);
4662
+ gap: var(--space-8);
4663
+ margin-inline-start: auto;
4806
4664
  }
4807
4665
 
4808
- /* Compact: by the screen width (the Figma viewport mode), as the SavingBar, not a container query. */
4809
4666
  @media (max-width: 1023px) {
4810
- .rds-form-actions--bar {
4811
- padding-block: 10px;
4812
- padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
4813
- }
4814
-
4815
- .rds-form-actions--bar .rds-form-actions__helper {
4816
- flex: 1 1 0;
4817
- min-width: 0;
4818
- }
4819
-
4820
- /* Only the primary (the last action): Cancelar is the topbar's back or X, or the details Drawer's footer. */
4821
- .rds-form-actions--bar .rds-form-actions__actions > :not(:last-child) {
4667
+ /* Only the primary (the last action): Cancelar is the topbar's X. */
4668
+ .rds-form-actions__actions > :not(:last-child) {
4822
4669
  display: none;
4823
4670
  }
4824
-
4825
- .rds-form-actions__helper--details > .rds-form-actions__text {
4826
- display: none;
4827
- }
4828
-
4829
- .rds-form-actions__details {
4830
- display: flex;
4831
- }
4832
- }
4833
-
4834
- @media (forced-colors: active) {
4835
- .rds-form-actions {
4836
- border-color: CanvasText;
4837
- }
4838
-
4839
- .rds-form-actions--bar {
4840
- border-top: var(--border-width) solid CanvasText;
4841
- }
4842
4671
  }
4843
4672
 
4844
4673
  /* components/google-account-chooser.css */
@@ -5561,6 +5390,124 @@ input:focus-visible + .rds-checkbox__box {
5561
5390
  }
5562
5391
  }
5563
5392
 
5393
+ /* components/internal/bottom-bar.css */
5394
+ /* @tokens bottom-bar */
5395
+ /*
5396
+ * The shell at the foot of the screen (Actions collection, bottom-bar/*), private, under the FormActions and the
5397
+ * SavingBar. The brand's primary colour: bottom-bar/background (colors/primary/default) with bottom-bar/text
5398
+ * (text/on/primary), in light, dark and every brand. Full width (at least 320), sticky at the bottom of the scrolling
5399
+ * container, bottom-bar/padding-x (24) at the sides, bottom-bar/gap (16) between the parts.
5400
+ * Height, by the screen width as the Figma viewport mode (layout/compact below lg 1024): compact, bottom-bar/padding-y
5401
+ * (10) above and below, 64 = 10 + 44 + 10, plus the safe area at the bottom; expanded, 2 more inside each side, 68.
5402
+ * The leading slot reserves bottom-bar/leading/min-width (44, the md Button height).
5403
+ * floating, from 1024 only: bottom-bar/floating/margin off the foot and the sides, at most
5404
+ * bottom-bar/floating/max-width, centred, bottom-bar/floating/radius and elevation/overlay. Below 1024 it is docked:
5405
+ * no radius, no shadow, no margin, the full width.
5406
+ * The Buttons on it stay the Button: the action fill and the neutral ghost read their tokens, redeclared here in the
5407
+ * bar's colours (bottom-bar/button/*): the fill's hover at 85% over the bar (active 70%), the ghost's hover the label
5408
+ * colour at 15% (active 25%), disabled at 40% over the bar (the Figma layer opacity), the focus ring
5409
+ * bottom-bar/focus/ring (the default focus/ring would vanish on the band).
5410
+ */
5411
+ .rds-bottom-bar {
5412
+ position: sticky;
5413
+ bottom: 0;
5414
+ z-index: var(--z-sticky);
5415
+ box-sizing: border-box;
5416
+ display: flex;
5417
+ align-items: center;
5418
+ gap: var(--bottom-bar-gap);
5419
+ width: 100%;
5420
+ min-width: min(320px, 100%);
5421
+ padding: calc(var(--bottom-bar-padding-y) + 2px) var(--bottom-bar-padding-x);
5422
+ background: var(--bottom-bar-background);
5423
+ color: var(--bottom-bar-text);
5424
+ font-family: var(--type-font-stack);
5425
+
5426
+ /* The Button tokens the action fill and the neutral ghost read, in the bar's colours. */
5427
+ --button-focus-ring: var(--bottom-bar-focus-ring);
5428
+ --button-action-fill-background-default: var(--bottom-bar-button-fill-background);
5429
+ --button-action-fill-background-hover: color-mix(in srgb, var(--bottom-bar-button-fill-background) 85%, var(--bottom-bar-background));
5430
+ --button-action-fill-background-active: color-mix(in srgb, var(--bottom-bar-button-fill-background) 70%, var(--bottom-bar-background));
5431
+ --button-action-fill-label: var(--bottom-bar-button-fill-label);
5432
+ --button-action-fill-icon: var(--bottom-bar-button-fill-label);
5433
+ --button-neutral-ghost-label: var(--bottom-bar-button-ghost-label);
5434
+ --button-neutral-ghost-icon: var(--bottom-bar-button-ghost-label);
5435
+ --button-neutral-ghost-background-hover: color-mix(in srgb, var(--bottom-bar-button-ghost-label) 15%, transparent);
5436
+ --button-neutral-ghost-background-active: color-mix(in srgb, var(--bottom-bar-button-ghost-label) 25%, transparent);
5437
+ --button-disabled-fill-background: color-mix(in srgb, var(--bottom-bar-button-fill-background) 40%, var(--bottom-bar-background));
5438
+ --button-disabled-fill-label: color-mix(in srgb, var(--bottom-bar-button-fill-label) 40%, var(--bottom-bar-background));
5439
+ --button-disabled-fill-icon: var(--button-disabled-fill-label);
5440
+ --button-disabled-ghost-label: color-mix(in srgb, var(--bottom-bar-button-ghost-label) 40%, var(--bottom-bar-background));
5441
+ --button-disabled-ghost-icon: var(--button-disabled-ghost-label);
5442
+ }
5443
+
5444
+ /* The leading slot: one ~44 control; its width stays reserved while the control comes and goes. */
5445
+ .rds-bottom-bar__leading {
5446
+ display: flex;
5447
+ flex: none;
5448
+ align-items: center;
5449
+ min-width: var(--bottom-bar-leading-min-width);
5450
+ }
5451
+
5452
+ /* Compact: by the screen width (the Figma viewport mode), not a container query. */
5453
+ @media (max-width: 1023px) {
5454
+ .rds-bottom-bar {
5455
+ padding-block: var(--bottom-bar-padding-y);
5456
+ padding-bottom: calc(var(--bottom-bar-padding-y) + env(safe-area-inset-bottom, 0px));
5457
+ }
5458
+ }
5459
+
5460
+ /* floating: only from lg 1024; below it the bar is docked, square, edge to edge. */
5461
+ @media (min-width: 1024px) {
5462
+ .rds-bottom-bar--floating {
5463
+ bottom: var(--bottom-bar-floating-margin);
5464
+ width: calc(100% - 2 * var(--bottom-bar-floating-margin));
5465
+ max-width: var(--bottom-bar-floating-max-width);
5466
+ margin-inline: auto;
5467
+ margin-bottom: var(--bottom-bar-floating-margin);
5468
+ border-radius: var(--bottom-bar-floating-radius);
5469
+ box-shadow: var(--elevation-overlay);
5470
+ }
5471
+ }
5472
+
5473
+ @media (forced-colors: active) {
5474
+ .rds-bottom-bar {
5475
+ border-top: var(--border-width) solid CanvasText;
5476
+ }
5477
+ }
5478
+
5479
+ @media (forced-colors: active) and (min-width: 1024px) {
5480
+ .rds-bottom-bar--floating {
5481
+ border: var(--border-width) solid CanvasText;
5482
+ }
5483
+ }
5484
+
5485
+ /*
5486
+ * The on-screen keyboard (compact only, the class comes from useOnScreenKeyboard): the bar slides down out of the way
5487
+ * while a field is being typed in, and comes back when the keyboard closes. visibility takes it out of the tab order
5488
+ * and the accessibility tree meanwhile; it waits for the slide to end before hiding.
5489
+ */
5490
+ @media (max-width: 1023px) {
5491
+ .rds-bottom-bar--keyboard {
5492
+ visibility: hidden;
5493
+ transform: translateY(100%);
5494
+ }
5495
+ }
5496
+
5497
+ @media (max-width: 1023px) and (prefers-reduced-motion: no-preference) {
5498
+ .rds-bottom-bar {
5499
+ transition:
5500
+ transform var(--motion-duration-base) var(--motion-easing-enter),
5501
+ visibility 0s;
5502
+ }
5503
+
5504
+ .rds-bottom-bar--keyboard {
5505
+ transition:
5506
+ transform var(--motion-duration-base) var(--motion-easing-exit),
5507
+ visibility 0s var(--motion-duration-base);
5508
+ }
5509
+ }
5510
+
5564
5511
  /* components/internal/choice.css */
5565
5512
  /* @tokens internal */
5566
5513
  /*
@@ -8662,61 +8609,15 @@ input:focus-visible + .rds-radio__circle {
8662
8609
  }
8663
8610
 
8664
8611
  /* components/saving-bar.css */
8665
- /* @tokens savingbar */
8612
+ /* @tokens bottom-bar */
8666
8613
  /*
8667
- * SavingBar (Actions collection, savingbar/*): full width (at least 320), 12 24 padding, 16 between the
8668
- * message (label, medium, at most 2 lines with an ellipsis) and the actions (8 apart). The bar is the brand's
8669
- * primary colour: savingbar/background is colors/primary/default and savingbar/text is text/on/primary (the text,
8670
- * the error alert, the chevron and the loader), so it follows the theme, light or dark, and every brand.
8671
- * The Buttons stay the Button: Salvar tone action fill and Descartar tone neutral ghost, with the bar's colours.
8672
- * The bar redeclares the Button tokens those two read: the Salvar fill is savingbar/button/fill/background with
8673
- * savingbar/button/fill/label, its hover the fill at 85% over the bar (active 70%); the Descartar label is
8674
- * savingbar/button/ghost/label, its hover the label colour at 15% (active 25%); the focus ring is savingbar/focus/ring;
8675
- * disabled (Descartar while saving) is the Button at 40% over the bar, as the Figma layer opacity. While saving, Salvar
8676
- * is in loading (its loader in the label colour): the only indicator, none in the message.
8677
- * Two arrangements, by the screen width, as the Figma viewport mode (layout/compact below lg 1024): expanded from 1024,
8678
- * 12 24 padding (68 tall), the message grows (at least 160) beside Descartar and Salvar; compact below it, one row,
8679
- * 10 24 padding (64 = 10 + 44 + 10) plus the safe area at the bottom, the message (flex 1, at most 2 lines) on the
8680
- * left and only the main button on the right: Descartar is not shown (it lives in the details Drawer's footer or in
8681
- * the confirmation when leaving).
8682
- * Details (showDetails, onDetails in code): in compact, the unsaved message becomes a text button with chevron-up 20 at
8683
- * the right (12 apart) that opens the Drawer with what is missing; hover underlines, focus draws the ring
8684
- * savingbar/focus/ring (focus/ring-width, radius/focus) 4 around the message box, inside stroke, as the Figma
8685
- * focus-ring frame. The touch area is 44 tall without changing the drawing. Expanded, the message stays text.
8614
+ * SavingBar: no tokens of its own; the shell (internal/bottom-bar.css, bottom-bar/*) brings the colour, the height,
8615
+ * the padding, the place and the Buttons' colours. Here the message (label, medium, in bottom-bar/text, at most 2
8616
+ * lines with an ellipsis; on error the alert 20 before it, 12 apart) and the actions (8 apart, Descartar then Salvar).
8617
+ * Expanded (from lg 1024) the message grows (at least 160) beside Descartar and Salvar; compact (below 1024, by the
8618
+ * screen width as the Figma viewport mode) one row, the message (flex 1) on the left and only Salvar on the right:
8619
+ * Descartar is not shown (it lives in the confirmation when leaving).
8686
8620
  */
8687
- .rds-savingbar {
8688
- box-sizing: border-box;
8689
- width: 100%;
8690
- min-width: min(320px, 100%);
8691
- padding: var(--space-12) var(--space-24);
8692
- background: var(--savingbar-background);
8693
- color: var(--savingbar-text);
8694
- font-family: var(--type-font-stack);
8695
-
8696
- /* The Button tokens Salvar (action fill) and Descartar (neutral ghost) read, in the bar's colours. */
8697
- --button-focus-ring: var(--savingbar-focus-ring);
8698
- --button-action-fill-background-default: var(--savingbar-button-fill-background);
8699
- --button-action-fill-background-hover: color-mix(in srgb, var(--savingbar-button-fill-background) 85%, var(--savingbar-background));
8700
- --button-action-fill-background-active: color-mix(in srgb, var(--savingbar-button-fill-background) 70%, var(--savingbar-background));
8701
- --button-action-fill-label: var(--savingbar-button-fill-label);
8702
- --button-action-fill-icon: var(--savingbar-button-fill-label);
8703
- --button-neutral-ghost-label: var(--savingbar-button-ghost-label);
8704
- --button-neutral-ghost-icon: var(--savingbar-button-ghost-label);
8705
- --button-neutral-ghost-background-hover: color-mix(in srgb, var(--savingbar-button-ghost-label) 15%, transparent);
8706
- --button-neutral-ghost-background-active: color-mix(in srgb, var(--savingbar-button-ghost-label) 25%, transparent);
8707
- --button-disabled-fill-background: color-mix(in srgb, var(--savingbar-button-fill-background) 40%, var(--savingbar-background));
8708
- --button-disabled-fill-label: color-mix(in srgb, var(--savingbar-button-fill-label) 40%, var(--savingbar-background));
8709
- --button-disabled-fill-icon: var(--button-disabled-fill-label);
8710
- --button-disabled-ghost-label: color-mix(in srgb, var(--savingbar-button-ghost-label) 40%, var(--savingbar-background));
8711
- --button-disabled-ghost-icon: var(--button-disabled-ghost-label);
8712
- }
8713
-
8714
- .rds-savingbar__row {
8715
- display: flex;
8716
- align-items: center;
8717
- gap: var(--space-16);
8718
- }
8719
-
8720
8621
  .rds-savingbar__message {
8721
8622
  display: flex;
8722
8623
  flex: 1 1 160px;
@@ -8757,51 +8658,7 @@ input:focus-visible + .rds-radio__circle {
8757
8658
  gap: var(--space-8);
8758
8659
  }
8759
8660
 
8760
- .rds-savingbar__details {
8761
- position: relative;
8762
- display: none;
8763
- flex: 1;
8764
- align-items: center;
8765
- gap: var(--space-12);
8766
- min-width: 0;
8767
- margin: 0;
8768
- padding: 0;
8769
- border: 0;
8770
- border-radius: var(--radius-focus);
8771
- background: none;
8772
- color: inherit;
8773
- font: inherit;
8774
- text-align: start;
8775
- cursor: pointer;
8776
- }
8777
-
8778
- /* The touch area: 44 tall around the message, invisible. */
8779
- .rds-savingbar__details::after {
8780
- content: '';
8781
- position: absolute;
8782
- top: 50%;
8783
- right: 0;
8784
- left: 0;
8785
- min-height: 44px;
8786
- height: 100%;
8787
- transform: translateY(-50%);
8788
- }
8789
-
8790
- .rds-savingbar__details:hover .rds-savingbar__text {
8791
- text-decoration: underline;
8792
- }
8793
-
8794
- .rds-savingbar__details:focus-visible {
8795
- outline: var(--focus-ring-width) solid var(--savingbar-focus-ring);
8796
- outline-offset: var(--focus-ring-width);
8797
- }
8798
-
8799
8661
  @media (max-width: 1023px) {
8800
- .rds-savingbar {
8801
- padding-block: 10px;
8802
- padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
8803
- }
8804
-
8805
8662
  .rds-savingbar__message {
8806
8663
  flex: 1 1 0;
8807
8664
  min-width: 0;
@@ -8810,20 +8667,6 @@ input:focus-visible + .rds-radio__circle {
8810
8667
  .rds-savingbar .rds-savingbar__discard {
8811
8668
  display: none;
8812
8669
  }
8813
-
8814
- .rds-savingbar__message--details > .rds-savingbar__text {
8815
- display: none;
8816
- }
8817
-
8818
- .rds-savingbar__details {
8819
- display: flex;
8820
- }
8821
- }
8822
-
8823
- @media (forced-colors: active) {
8824
- .rds-savingbar {
8825
- border-top: var(--border-width) solid CanvasText;
8826
- }
8827
8670
  }
8828
8671
 
8829
8672
  /* components/section-card.css */
@@ -10289,8 +10132,18 @@ input:focus-visible + .rds-switch__track {
10289
10132
  * A line under every row but the last: the Table's border closes it. Corners in radius/container; the sort button in
10290
10133
  * radius/xs. The status rows hold an Empty, 32 24 around it.
10291
10134
  */
10135
+ /*
10136
+ * The scroll region is a one-column grid, minmax(0, 1fr), so it does not hand the table's min-content up to the page:
10137
+ * as a plain block it did, and any ancestor sized by its content (a flex item without min-width: 0, a grid item in an
10138
+ * auto track, a start-aligned stack, an inline-block) grew to the table's width, so a Card holding a wide table went
10139
+ * past the screen and was cut instead of the table scrolling. Under a min-content constraint the fr track is 0; under
10140
+ * max-content it is the table's max-content (a hugging parent still hugs the table); with a definite width it fills,
10141
+ * and the table, never narrower than its min-content, scrolls inside.
10142
+ */
10292
10143
  .rds-table {
10293
10144
  box-sizing: border-box;
10145
+ display: grid;
10146
+ grid-template-columns: minmax(0, 1fr);
10294
10147
  max-width: 100%;
10295
10148
  overflow-x: auto;
10296
10149
  border: var(--border-width) solid var(--table-border);
@@ -73,7 +73,7 @@
73
73
  --button-inverse-focus-ring: var(--text-on-primary-strong);
74
74
  --button-inverse-disabled-fill-background: var(--colors-primary-light);
75
75
  --button-inverse-disabled-label: var(--text-disabled-invert);
76
- --savingbar-background: var(--colors-primary-default);
76
+ --bottom-bar-background: var(--colors-primary-default);
77
77
  --toggle-label: var(--text-body);
78
78
  --toggle-border: var(--border-strong);
79
79
  --toggle-background-hover: var(--surface-muted);
@@ -83,7 +83,7 @@
83
83
  --toggle-label-disabled: var(--text-disabled);
84
84
  --toggle-border-pressed: var(--colors-primary-default);
85
85
  --toggle-border-disabled: var(--border-default);
86
- --savingbar-text: var(--text-on-primary);
86
+ --bottom-bar-text: var(--text-on-primary);
87
87
  --fab-cta-background-default: var(--colors-primary-default);
88
88
  --fab-cta-background-hover: var(--colors-primary-active);
89
89
  --fab-cta-text: var(--text-on-primary);
@@ -93,9 +93,6 @@
93
93
  --chip-label: var(--text-body);
94
94
  --chip-icon: var(--text-muted);
95
95
  --chip-label-disabled: var(--text-disabled);
96
- --form-actions-background: var(--surface-card);
97
- --form-actions-border: var(--border-default);
98
- --form-actions-text: var(--text-subtle);
99
96
  --button-size-sm-height: 36px;
100
97
  --button-size-sm-padding-x: var(--space-16);
101
98
  --button-size-sm-padding-y: var(--space-8);
@@ -120,12 +117,17 @@
120
117
  --button-size-lg-icon-only: var(--size-24);
121
118
  --button-size-lg-text-size: var(--type-body-size);
122
119
  --button-size-lg-text-line: var(--type-body-line);
123
- --savingbar-focus-ring: var(--text-on-primary);
124
- --savingbar-button-fill-background: var(--text-on-primary);
125
- --savingbar-button-fill-label: var(--colors-primary-default);
126
- --savingbar-button-ghost-label: var(--text-on-primary);
127
- --form-actions-bar-background: var(--surface-page);
128
- --form-actions-focus-ring: var(--focus-ring);
120
+ --bottom-bar-focus-ring: var(--text-on-primary);
121
+ --bottom-bar-button-fill-background: var(--text-on-primary);
122
+ --bottom-bar-button-fill-label: var(--colors-primary-default);
123
+ --bottom-bar-button-ghost-label: var(--text-on-primary);
124
+ --bottom-bar-padding-x: var(--space-24);
125
+ --bottom-bar-padding-y: 10px;
126
+ --bottom-bar-gap: var(--space-16);
127
+ --bottom-bar-leading-min-width: var(--button-size-md-height);
128
+ --bottom-bar-floating-radius: var(--radius-container);
129
+ --bottom-bar-floating-margin: var(--space-24);
130
+ --bottom-bar-floating-max-width: 768px;
129
131
 
130
132
  /* blocks */
131
133
  --topnav-background: var(--surface-card);