@rojaostudio/ds 2.0.0-next.23 → 2.0.0-next.26

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,28 +1,60 @@
1
1
  import * as react from 'react';
2
2
  import { HTMLAttributes, ReactNode } from 'react';
3
3
 
4
- type FormActionsLayout = 'inline' | 'stacked';
4
+ type FormActionsLayout = 'inline' | 'stacked' | 'bar';
5
5
  interface FormActionsProps extends HTMLAttributes<HTMLDivElement> {
6
6
  /**
7
7
  * The buttons (Figma: slot `actions`, up to 3), always in reading order: Cancelar first, the primary
8
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.
9
+ * order follows what is seen. In `bar`, compact (below 1024), only the last one (the primary) is shown.
10
10
  */
11
11
  children: ReactNode;
12
- /** A short supporting sentence beside the actions (Figma: `showHelper` + `helper`). */
12
+ /**
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.
15
+ */
13
16
  helper?: ReactNode;
14
17
  /**
15
- * inline (desktop: floats and hugs its content, sentence left, actions right) or stacked (phone: edge
16
- * to edge at the foot, primary full width on top) (Figma: `layout`).
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`).
17
22
  */
18
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;
19
35
  }
20
36
  /**
21
37
  * FormActions — Figma [RDS] Actions/FormActions. The actions at the end of a create form or a task,
22
38
  * there from the first second. Where it sticks is the page's job: `position: sticky` at the bottom of
23
- * the scrolling container, with `scroll-padding-bottom` equal to its height, so a focused field is never
24
- * hidden behind it (WCAG 2.4.11). Styles: form-actions.css.
39
+ * the scrolling container. Styles: form-actions.css.
40
+ *
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.
47
+ *
48
+ * 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.
25
57
  */
26
- declare function FormActions({ children, helper, layout, className, ...rest }: FormActionsProps): react.JSX.Element;
58
+ declare function FormActions({ children, helper, layout, onDetails, detailsLabel, detailsExpanded, detailsControls, className, ...rest }: FormActionsProps): react.JSX.Element;
27
59
 
28
60
  export { FormActions, type FormActionsLayout, type FormActionsProps };
@@ -3,17 +3,65 @@ import {
3
3
  __spreadProps,
4
4
  __spreadValues
5
5
  } from "../chunk-ORMEWXMH.js";
6
- import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { Children } from "react";
8
+ import { ChevronUpIcon } from "./internal/icons.js";
8
9
  function FormActions(_a) {
9
- var _b = _a, { children, helper, layout = "inline", className } = _b, rest = __objRest(_b, ["children", "helper", "layout", "className"]);
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
+ ]);
10
29
  const actions = Children.toArray(children);
30
+ const bar = layout === "bar";
31
+ const details = bar && helper != null && helper !== false && onDetails !== void 0;
11
32
  return /* @__PURE__ */ jsxs(
12
33
  "div",
13
34
  __spreadProps(__spreadValues({}, rest), {
14
35
  className: ["rds-form-actions", `rds-form-actions--${layout}`, className].filter(Boolean).join(" "),
15
36
  children: [
16
- helper && /* @__PURE__ */ jsx("p", { className: "rds-form-actions__helper", children: helper }),
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
+ ),
17
65
  /* @__PURE__ */ jsx("div", { className: "rds-form-actions__actions", children: layout === "stacked" ? actions.reverse() : actions })
18
66
  ]
19
67
  })
@@ -4,6 +4,7 @@ export { HoverCard, HoverCardProps } from './hover-card.js';
4
4
  export { Dialog, DialogClose, DialogProps, DialogSize } from './dialog.js';
5
5
  export { Sheet, SheetClose, SheetProps, SheetSide } from './sheet.js';
6
6
  export { Drawer, DrawerClose, DrawerProps } from './drawer.js';
7
+ export { LoadingOverlay, LoadingOverlayProps } from './loading-overlay.js';
7
8
  export { DropdownMenu, DropdownMenuAlign, DropdownMenuItem, DropdownMenuItemProps, DropdownMenuItemTone, DropdownMenuProps, DropdownMenuRadioGroup, DropdownMenuRadioGroupProps, DropdownMenuRadioItem, DropdownMenuRadioItemProps, DropdownMenuSeparator, DropdownMenuSide } from './dropdown-menu.js';
8
9
  export { ContextMenu, ContextMenuItem, ContextMenuItemProps, ContextMenuItemTone, ContextMenuProps, ContextMenuSeparator } from './context-menu.js';
9
10
  export { Command, CommandGroup, CommandGroupProps, CommandItem, CommandItemProps, CommandProps } from './command.js';
@@ -5,6 +5,7 @@ import { HoverCard } from "./hover-card.js";
5
5
  import { Dialog, DialogClose } from "./dialog.js";
6
6
  import { Sheet, SheetClose } from "./sheet.js";
7
7
  import { Drawer, DrawerClose } from "./drawer.js";
8
+ import { LoadingOverlay } from "./loading-overlay.js";
8
9
  import {
9
10
  DropdownMenu,
10
11
  DropdownMenuItem,
@@ -223,6 +224,7 @@ export {
223
224
  KbdGroup,
224
225
  Listbox,
225
226
  ListboxOption,
227
+ LoadingOverlay,
226
228
  Marker,
227
229
  MediaTile,
228
230
  Message,
@@ -0,0 +1,43 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes } from 'react';
3
+
4
+ interface LoadingOverlayProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
5
+ /**
6
+ * The action is running (Figma: `open`, code only). The overlay shows only if it is still true after `delay`:
7
+ * if the next page opens first, it never shows.
8
+ */
9
+ open: boolean;
10
+ /** What is happening, in the gerund with its object: "Criando o produto…" (Figma: `label`). It is the dialog's name and is said once (aria-live="polite"). */
11
+ label?: string;
12
+ /**
13
+ * How long (ms) `open` has to stay true before the overlay shows. Up to it, the wait is the main button in
14
+ * loading (`<Button loading>`, or the SavingBar in `status="saving"`); past it, the overlay comes in and stays
15
+ * until the other page opens. 400 by default; 0 shows it at once.
16
+ */
17
+ delay?: number;
18
+ /** Where the portal goes (default: document.body). Inside a `.ds-plate`, for example, to read the brand mode. */
19
+ container?: HTMLElement | null;
20
+ }
21
+ /**
22
+ * LoadingOverlay — Figma [RDS] Overlays/LoadingOverlay. The long wait of an action that leads to another page
23
+ * (create the product and land on its editing). A veil over the whole screen (loading-overlay/scrim) and, in the
24
+ * middle, a panel (loading-overlay/background, radius/container, elevation/modal) with the Spinner lg and one line
25
+ * saying what is happening. Styles: loading-overlay.css.
26
+ *
27
+ * Time: up to `delay` (~400 ms) only the main button is in loading ("Criando…", aria-busy on the button); past it,
28
+ * the overlay shows, and it stays until the other page opens. If the navigation comes first, it never shows. Do not
29
+ * use it for short waits (it only flashes), to load a page or a list (PageSkeleton), for a measured wait (Progress),
30
+ * or to save without changing page (the SavingBar in `status="saving"`). Never two indicators: with the overlay,
31
+ * the SavingBar keeps only its button in loading.
32
+ *
33
+ * Error: the app sets `open={false}` (the overlay leaves and the focus goes back to where it was, the main button)
34
+ * and returns the error to the SavingBar in `status="error"`, with the form untouched.
35
+ *
36
+ * Accessibility: role="dialog" aria-modal="true" named by `label`; the focus goes to the panel (tabindex=-1) and
37
+ * stays there; Escape does not close it (the action is running). While it is shown, the rest of the page is
38
+ * `inert` and `aria-busy="true"` (put back on close). The label is in an aria-live="polite" region. With
39
+ * prefers-reduced-motion: reduce, no fade, and the Spinner turns slowly (3 s a turn).
40
+ */
41
+ declare function LoadingOverlay({ open, label, delay, container, className, ...rest }: LoadingOverlayProps): react.ReactPortal | null;
42
+
43
+ export { LoadingOverlay, type LoadingOverlayProps };
@@ -0,0 +1,81 @@
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 { useEffect, useLayoutEffect, useRef, useState } from "react";
9
+ import { createPortal } from "react-dom";
10
+ import { Spinner } from "./spinner.js";
11
+ function isolate(keep) {
12
+ const marked = [];
13
+ for (let node = keep; node && node !== document.body && node.parentElement; node = node.parentElement) {
14
+ for (const sibling of node.parentElement.children) {
15
+ if (sibling === node || sibling.hasAttribute("inert") || /^(SCRIPT|STYLE|LINK|TEMPLATE)$/.test(sibling.tagName)) continue;
16
+ marked.push({ el: sibling, inert: sibling.getAttribute("inert"), busy: sibling.getAttribute("aria-busy") });
17
+ sibling.setAttribute("inert", "");
18
+ sibling.setAttribute("aria-busy", "true");
19
+ }
20
+ }
21
+ return marked;
22
+ }
23
+ function restore(marked) {
24
+ for (const { el, inert, busy } of marked) {
25
+ if (inert === null) el.removeAttribute("inert");
26
+ else el.setAttribute("inert", inert);
27
+ if (busy === null) el.removeAttribute("aria-busy");
28
+ else el.setAttribute("aria-busy", busy);
29
+ }
30
+ }
31
+ function LoadingOverlay(_a) {
32
+ var _b = _a, { open, label = "Carregando\u2026", delay = 400, container, className } = _b, rest = __objRest(_b, ["open", "label", "delay", "container", "className"]);
33
+ const [shown, setShown] = useState(false);
34
+ const veil = useRef(null);
35
+ const panel = useRef(null);
36
+ useEffect(() => {
37
+ if (!open) {
38
+ setShown(false);
39
+ return;
40
+ }
41
+ if (delay <= 0) {
42
+ setShown(true);
43
+ return;
44
+ }
45
+ const timer = setTimeout(() => setShown(true), delay);
46
+ return () => clearTimeout(timer);
47
+ }, [open, delay]);
48
+ useLayoutEffect(() => {
49
+ if (!shown || !veil.current || !panel.current) return;
50
+ const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
51
+ const marked = isolate(veil.current);
52
+ const box = panel.current;
53
+ box.focus({ preventScroll: true });
54
+ const keep = (event) => {
55
+ if (!box.contains(event.target)) box.focus({ preventScroll: true });
56
+ };
57
+ document.addEventListener("focusin", keep);
58
+ return () => {
59
+ document.removeEventListener("focusin", keep);
60
+ restore(marked);
61
+ if (previous == null ? void 0 : previous.isConnected) previous.focus({ preventScroll: true });
62
+ };
63
+ }, [shown]);
64
+ if (!shown || typeof document === "undefined") return null;
65
+ const onKeyDown = (event) => {
66
+ if (event.key === "Escape" || event.key === "Tab") {
67
+ event.preventDefault();
68
+ event.stopPropagation();
69
+ }
70
+ };
71
+ return createPortal(
72
+ /* @__PURE__ */ jsx("div", __spreadProps(__spreadValues({}, rest), { ref: veil, className: ["rds-loading-overlay", className].filter(Boolean).join(" "), onKeyDown, children: /* @__PURE__ */ jsxs("div", { ref: panel, className: "rds-loading-overlay__panel", role: "dialog", "aria-modal": "true", "aria-label": label, tabIndex: -1, children: [
73
+ /* @__PURE__ */ jsx(Spinner, { size: "lg", tone: "neutral", "aria-hidden": "true" }),
74
+ /* @__PURE__ */ jsx("p", { className: "rds-loading-overlay__label", "aria-live": "polite", children: label })
75
+ ] }) })),
76
+ container != null ? container : document.body
77
+ );
78
+ }
79
+ export {
80
+ LoadingOverlay
81
+ };
@@ -4,19 +4,26 @@ import { HTMLAttributes, ReactNode } from 'react';
4
4
  type SavingBarStatus = 'unsaved' | 'saving' | 'error';
5
5
  interface SavingBarProps extends HTMLAttributes<HTMLDivElement> {
6
6
  /**
7
- * unsaved: the message and the actions; saving: a turning loader and `savingMessage`, buttons off;
8
- * error: an alert, `errorMessage`, and Salvar becomes `retryLabel` (Figma: `status`).
7
+ * unsaved: the message and the actions; saving: `savingMessage`, Salvar in loading (`savingLabel`, the only
8
+ * indicator, no loader in the message) and Descartar disabled; error: an alert, `errorMessage`, and Salvar
9
+ * becomes `retryLabel` (Figma: `status`).
9
10
  */
10
11
  status?: SavingBarStatus;
11
12
  /** The unsaved text (Figma: `message`). */
12
13
  message?: ReactNode;
13
14
  /** The text while saving. */
14
15
  savingMessage?: ReactNode;
16
+ /** Salvar's label while saving, in loading (`<Button loading>`), also announced. */
17
+ savingLabel?: string;
15
18
  /** The error text. */
16
19
  errorMessage?: ReactNode;
17
20
  /** Called by Salvar (and by Tentar de novo, on error). */
18
21
  onSave: () => void;
19
- /** Shows Descartar and is called when it is pressed (Figma: `showDiscard`). */
22
+ /**
23
+ * 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
+ */
20
27
  onDiscard?: () => void;
21
28
  saveLabel?: string;
22
29
  retryLabel?: string;
@@ -38,8 +45,11 @@ interface SavingBarProps extends HTMLAttributes<HTMLDivElement> {
38
45
  * saved, and leaves once saved. It is the brand's primary colour (savingbar/background, colors/primary/default),
39
46
  * in light and dark and in every brand: a state that waits for a decision, not a notice. It does not place itself:
40
47
  * put it sticky or fixed at the bottom. Its Buttons are Salvar in tone action and Descartar in tone neutral ghost,
41
- * with the bar's colours (savingbar/button/*). Styles: saving-bar.css.
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.
42
52
  */
43
- declare function SavingBar({ status, message, savingMessage, errorMessage, onSave, onDiscard, saveLabel, retryLabel, discardLabel, onDetails, detailsLabel, detailsExpanded, detailsControls, className, ...rest }: SavingBarProps): react.JSX.Element;
53
+ declare function SavingBar({ status, message, savingMessage, savingLabel, errorMessage, onSave, onDiscard, saveLabel, retryLabel, discardLabel, onDetails, detailsLabel, detailsExpanded, detailsControls, className, ...rest }: SavingBarProps): react.JSX.Element;
44
54
 
45
55
  export { SavingBar, type SavingBarProps, type SavingBarStatus };
@@ -6,13 +6,13 @@ 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 { Loader } from "./internal/button.js";
10
9
  import { AlertIcon, ChevronUpIcon } from "./internal/icons.js";
11
10
  function SavingBar(_a) {
12
11
  var _b = _a, {
13
12
  status = "unsaved",
14
13
  message = "Altera\xE7\xF5es n\xE3o salvas",
15
14
  savingMessage = "Salvando\u2026",
15
+ savingLabel = "Salvando\u2026",
16
16
  errorMessage = "N\xE3o salvou. Tente de novo.",
17
17
  onSave,
18
18
  onDiscard,
@@ -28,6 +28,7 @@ function SavingBar(_a) {
28
28
  "status",
29
29
  "message",
30
30
  "savingMessage",
31
+ "savingLabel",
31
32
  "errorMessage",
32
33
  "onSave",
33
34
  "onDiscard",
@@ -53,7 +54,6 @@ function SavingBar(_a) {
53
54
  className: ["rds-savingbar", className].filter(Boolean).join(" "),
54
55
  children: /* @__PURE__ */ jsxs("div", { className: "rds-savingbar__row", children: [
55
56
  /* @__PURE__ */ jsxs("p", { className: ["rds-savingbar__message", details && "rds-savingbar__message--details"].filter(Boolean).join(" "), role: "status", children: [
56
- saving && /* @__PURE__ */ jsx("span", { className: "rds-savingbar__icon rds-savingbar__loader", "aria-hidden": "true", children: /* @__PURE__ */ jsx(Loader, {}) }),
57
57
  error && /* @__PURE__ */ jsx("span", { className: "rds-savingbar__icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx(AlertIcon, {}) }),
58
58
  /* @__PURE__ */ jsx("span", { className: "rds-savingbar__text", children: text }),
59
59
  details && /* @__PURE__ */ jsxs(
@@ -76,8 +76,8 @@ function SavingBar(_a) {
76
76
  )
77
77
  ] }),
78
78
  /* @__PURE__ */ jsxs("div", { className: "rds-savingbar__actions", children: [
79
- onDiscard && /* @__PURE__ */ jsx(Button, { tone: "neutral", variant: "ghost", disabled: saving, onClick: onDiscard, children: discardLabel }),
80
- /* @__PURE__ */ jsx(Button, { tone: "action", variant: "fill", disabled: saving, onClick: onSave, children: error ? retryLabel : saveLabel })
79
+ onDiscard && /* @__PURE__ */ jsx(Button, { className: "rds-savingbar__discard", tone: "neutral", variant: "ghost", disabled: saving, onClick: onDiscard, children: discardLabel }),
80
+ /* @__PURE__ */ jsx(Button, { tone: "action", variant: "fill", loading: saving, loadingLabel: savingLabel, onClick: onSave, children: saving ? savingLabel : error ? retryLabel : saveLabel })
81
81
  ] })
82
82
  ] })
83
83
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rojaostudio/ds",
3
- "version": "2.0.0-next.23",
3
+ "version": "2.0.0-next.26",
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",
@@ -190,7 +190,7 @@
190
190
  "@radix-ui/react-toggle": "^1.1.18",
191
191
  "@radix-ui/react-toggle-group": "^1.1.19",
192
192
  "@radix-ui/react-tooltip": "^1.2.16",
193
- "@rojaostudio/ds-core": "1.1.0-next.9"
193
+ "@rojaostudio/ds-core": "1.1.0-next.10"
194
194
  },
195
195
  "scripts": {
196
196
  "build": "pnpm run build:tokens && pnpm run build:themes && pnpm run build:rds && pnpm run build:native && tsup",
@@ -4639,6 +4639,16 @@ input:focus-visible + .rds-checkbox__box {
4639
4639
  * FormActions (Actions collection, form-actions/*): the actions at the end of a form, on the card
4640
4640
  * surface with a hairline. inline floats (container radius, raised elevation, 16 24 padding, 24 between
4641
4641
  * sentence and actions); stacked sticks to the foot edge to edge (16 padding, hairline on top).
4642
+ * bar is the foot of a create form, the SavingBar's skeleton but neutral: form-actions/bar/background
4643
+ * (surface/page) with the hairline form-actions/border on top (drawn inside, so it adds no height), full
4644
+ * width (at least 320), 16 between the helper and the actions (8 apart). Two arrangements by the screen
4645
+ * width, as the SavingBar (layout/compact below lg 1024): expanded, 12 24 padding (68 = 12 + 44 + 12), the
4646
+ * helper grows (at least 160) beside Cancelar and the primary; compact, one row, 10 24 padding
4647
+ * (64 = 10 + 44 + 10) plus the safe area at the bottom, the helper (flex 1, at most 2 lines) on the left and
4648
+ * only the last action (the primary) on the right.
4649
+ * Details (showDetails, onDetails in code): compact, the helper becomes a text button with chevron-up 20 at
4650
+ * the right (12 apart); hover underlines, focus draws form-actions/focus/ring (focus/ring-width, radius/focus);
4651
+ * the touch area is 44 tall without changing the drawing. Expanded, the helper stays text.
4642
4652
  */
4643
4653
  .rds-form-actions {
4644
4654
  box-sizing: border-box;
@@ -4694,10 +4704,125 @@ input:focus-visible + .rds-checkbox__box {
4694
4704
  width: 100%;
4695
4705
  }
4696
4706
 
4707
+ /* bar: the foot of a create form, the SavingBar's skeleton, neutral. */
4708
+ .rds-form-actions--bar {
4709
+ gap: var(--space-16);
4710
+ justify-content: flex-end;
4711
+ width: 100%;
4712
+ min-width: min(320px, 100%);
4713
+ padding: var(--space-12) var(--space-24);
4714
+ background: var(--form-actions-bar-background);
4715
+ /* The hairline on top, inside: the bar stays 68 / 64 tall. */
4716
+ box-shadow: inset 0 var(--border-width) 0 var(--form-actions-border);
4717
+ }
4718
+
4719
+ .rds-form-actions--bar .rds-form-actions__helper {
4720
+ display: flex;
4721
+ flex: 1 1 160px;
4722
+ align-items: center;
4723
+ min-width: 160px;
4724
+ }
4725
+
4726
+ .rds-form-actions--bar .rds-form-actions__actions {
4727
+ flex: none;
4728
+ flex-wrap: nowrap;
4729
+ }
4730
+
4731
+ .rds-form-actions__text {
4732
+ display: -webkit-box;
4733
+ flex: 1;
4734
+ min-width: 0;
4735
+ overflow: hidden;
4736
+ -webkit-box-orient: vertical;
4737
+ -webkit-line-clamp: 2;
4738
+ line-clamp: 2;
4739
+ }
4740
+
4741
+ .rds-form-actions__icon {
4742
+ display: inline-flex;
4743
+ flex: none;
4744
+ width: var(--size-20);
4745
+ height: var(--size-20);
4746
+ }
4747
+
4748
+ .rds-form-actions__icon > svg {
4749
+ width: 100%;
4750
+ height: 100%;
4751
+ }
4752
+
4753
+ .rds-form-actions__details {
4754
+ position: relative;
4755
+ display: none;
4756
+ flex: 1;
4757
+ align-items: center;
4758
+ gap: var(--space-12);
4759
+ min-width: 0;
4760
+ margin: 0;
4761
+ padding: 0;
4762
+ border: 0;
4763
+ border-radius: var(--radius-focus);
4764
+ background: none;
4765
+ color: inherit;
4766
+ font: inherit;
4767
+ text-align: start;
4768
+ cursor: pointer;
4769
+ }
4770
+
4771
+ /* The touch area: 44 tall around the helper, invisible. */
4772
+ .rds-form-actions__details::after {
4773
+ content: '';
4774
+ position: absolute;
4775
+ top: 50%;
4776
+ right: 0;
4777
+ left: 0;
4778
+ min-height: 44px;
4779
+ height: 100%;
4780
+ transform: translateY(-50%);
4781
+ }
4782
+
4783
+ .rds-form-actions__details:hover .rds-form-actions__text {
4784
+ text-decoration: underline;
4785
+ }
4786
+
4787
+ .rds-form-actions__details:focus-visible {
4788
+ outline: var(--focus-ring-width) solid var(--form-actions-focus-ring);
4789
+ outline-offset: var(--focus-ring-width);
4790
+ }
4791
+
4792
+ /* Compact: by the screen width (the Figma viewport mode), as the SavingBar, not a container query. */
4793
+ @media (max-width: 1023px) {
4794
+ .rds-form-actions--bar {
4795
+ padding-block: 10px;
4796
+ padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
4797
+ }
4798
+
4799
+ .rds-form-actions--bar .rds-form-actions__helper {
4800
+ flex: 1 1 0;
4801
+ min-width: 0;
4802
+ }
4803
+
4804
+ /* Only the primary (the last action): Cancelar is the topbar's back or X, or the details Drawer's footer. */
4805
+ .rds-form-actions--bar .rds-form-actions__actions > :not(:last-child) {
4806
+ display: none;
4807
+ }
4808
+
4809
+ .rds-form-actions__helper--details > .rds-form-actions__text {
4810
+ display: none;
4811
+ }
4812
+
4813
+ .rds-form-actions__details {
4814
+ display: flex;
4815
+ }
4816
+ }
4817
+
4697
4818
  @media (forced-colors: active) {
4698
4819
  .rds-form-actions {
4699
4820
  border-color: CanvasText;
4700
4821
  }
4822
+
4823
+ .rds-form-actions--bar {
4824
+ border-top: var(--border-width) solid CanvasText;
4825
+ }
4701
4826
  }
4702
4827
 
4703
4828
  /* components/google-account-chooser.css */
@@ -6783,6 +6908,70 @@ body:has(.rds-modal) {
6783
6908
  }
6784
6909
  }
6785
6910
 
6911
+ /* components/loading-overlay.css */
6912
+ /* @tokens loading-overlay */
6913
+ /*
6914
+ * LoadingOverlay (Overlays collection, loading-overlay/*): a veil over the whole screen (loading-overlay/scrim,
6915
+ * surface/scrim) in the overlay layer (z/overlay), and a panel in the middle (loading-overlay/background,
6916
+ * surface/card; radius/container, 24 padding, 16 between the Spinner lg and the label, at most 320 wide,
6917
+ * elevation/modal). The label is type/label Medium in loading-overlay/text (text/body), centred. The panel gives the
6918
+ * text its own surface, so it reads over any screen, light, dark or in the brand. It fades in; with reduced motion,
6919
+ * no fade (and the Spinner turns slowly, spinner.css).
6920
+ */
6921
+ .rds-loading-overlay {
6922
+ position: fixed;
6923
+ inset: 0;
6924
+ z-index: var(--z-overlay);
6925
+ display: flex;
6926
+ align-items: center;
6927
+ justify-content: center;
6928
+ padding: var(--space-16);
6929
+ background: var(--loading-overlay-scrim);
6930
+ animation: rds-loading-overlay-in 150ms ease-out;
6931
+ }
6932
+
6933
+ .rds-loading-overlay__panel {
6934
+ box-sizing: border-box;
6935
+ display: flex;
6936
+ flex-direction: column;
6937
+ align-items: center;
6938
+ gap: var(--space-16);
6939
+ max-width: min(320px, 100%);
6940
+ padding: var(--space-24);
6941
+ border-radius: var(--radius-container);
6942
+ background: var(--loading-overlay-background);
6943
+ box-shadow: var(--elevation-modal);
6944
+ color: var(--loading-overlay-text);
6945
+ font-family: var(--type-font-stack);
6946
+ outline: none;
6947
+ }
6948
+
6949
+ .rds-loading-overlay__label {
6950
+ margin: 0;
6951
+ font-size: var(--type-label-size);
6952
+ font-weight: 500;
6953
+ line-height: var(--type-label-line);
6954
+ text-align: center;
6955
+ }
6956
+
6957
+ @keyframes rds-loading-overlay-in {
6958
+ from {
6959
+ opacity: 0;
6960
+ }
6961
+ }
6962
+
6963
+ @media (prefers-reduced-motion: reduce) {
6964
+ .rds-loading-overlay {
6965
+ animation: none;
6966
+ }
6967
+ }
6968
+
6969
+ @media (forced-colors: active) {
6970
+ .rds-loading-overlay__panel {
6971
+ border: var(--border-width) solid CanvasText;
6972
+ }
6973
+ }
6974
+
6786
6975
  /* components/marker.css */
6787
6976
  /* @tokens marker */
6788
6977
  /*
@@ -8467,10 +8656,13 @@ input:focus-visible + .rds-radio__circle {
8467
8656
  * The bar redeclares the Button tokens those two read: the Salvar fill is savingbar/button/fill/background with
8468
8657
  * savingbar/button/fill/label, its hover the fill at 85% over the bar (active 70%); the Descartar label is
8469
8658
  * savingbar/button/ghost/label, its hover the label colour at 15% (active 25%); the focus ring is savingbar/focus/ring;
8470
- * disabled (saving) is each Button at 40% over the bar, as the Figma layer opacity.
8659
+ * disabled (Descartar while saving) is the Button at 40% over the bar, as the Figma layer opacity. While saving, Salvar
8660
+ * is in loading (its loader in the label colour): the only indicator, none in the message.
8471
8661
  * Two arrangements, by the screen width, as the Figma viewport mode (layout/compact below lg 1024): expanded from 1024,
8472
- * the message grows (at least 160) beside the actions; compact below it, the message on top and Descartar and
8473
- * Salvar under it, sharing the width.
8662
+ * 12 24 padding (68 tall), the message grows (at least 160) beside Descartar and Salvar; compact below it, one row,
8663
+ * 10 24 padding (64 = 10 + 44 + 10) plus the safe area at the bottom, the message (flex 1, at most 2 lines) on the
8664
+ * left and only the main button on the right: Descartar is not shown (it lives in the details Drawer's footer or in
8665
+ * the confirmation when leaving).
8474
8666
  * Details (showDetails, onDetails in code): in compact, the unsaved message becomes a text button with chevron-up 20 at
8475
8667
  * the right (12 apart) that opens the Drawer with what is missing; hover underlines, focus draws the ring
8476
8668
  * savingbar/focus/ring (focus/ring-width, radius/focus) 4 around the message box, inside stroke, as the Figma
@@ -8543,22 +8735,6 @@ input:focus-visible + .rds-radio__circle {
8543
8735
  height: 100%;
8544
8736
  }
8545
8737
 
8546
- .rds-savingbar__loader > svg {
8547
- animation: rds-savingbar-turn 1s linear infinite;
8548
- }
8549
-
8550
- @keyframes rds-savingbar-turn {
8551
- to {
8552
- transform: rotate(360deg);
8553
- }
8554
- }
8555
-
8556
- @media (prefers-reduced-motion: reduce) {
8557
- .rds-savingbar__loader > svg {
8558
- animation-duration: 3s;
8559
- }
8560
- }
8561
-
8562
8738
  .rds-savingbar__actions {
8563
8739
  display: flex;
8564
8740
  flex: none;
@@ -8605,19 +8781,18 @@ input:focus-visible + .rds-radio__circle {
8605
8781
  }
8606
8782
 
8607
8783
  @media (max-width: 1023px) {
8608
- .rds-savingbar__row {
8609
- flex-direction: column;
8610
- align-items: stretch;
8784
+ .rds-savingbar {
8785
+ padding-block: 10px;
8786
+ padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
8611
8787
  }
8612
8788
 
8613
8789
  .rds-savingbar__message {
8614
- flex: none;
8790
+ flex: 1 1 0;
8615
8791
  min-width: 0;
8616
8792
  }
8617
8793
 
8618
- .rds-savingbar__actions > * {
8619
- flex: 1 1 0;
8620
- min-width: 0;
8794
+ .rds-savingbar .rds-savingbar__discard {
8795
+ display: none;
8621
8796
  }
8622
8797
 
8623
8798
  .rds-savingbar__message--details > .rds-savingbar__text {
@@ -124,6 +124,8 @@
124
124
  --savingbar-button-fill-background: var(--text-on-primary);
125
125
  --savingbar-button-fill-label: var(--colors-primary-default);
126
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);
127
129
 
128
130
  /* blocks */
129
131
  --topnav-background: var(--surface-card);
@@ -908,11 +910,11 @@
908
910
  --progress-fill: var(--chart-series-1);
909
911
  --progress-label: var(--text-body);
910
912
  --progress-value: var(--text-muted);
911
- --spinner-indicator: var(--colors-primary-default);
913
+ --spinner-indicator: var(--colors-primary-dark);
912
914
  --spinner-track: var(--surface-tint-strong);
913
915
  --spinner-label: var(--text-muted);
914
- --spinner-inverse-indicator: var(--text-on-primary-strong);
915
- --spinner-inverse-label: var(--text-on-primary-strong);
916
+ --spinner-inverse-indicator: var(--text-on-primary);
917
+ --spinner-inverse-label: var(--text-on-primary);
916
918
  --rating-empty: var(--border-strong);
917
919
  --rating-label: var(--text-muted);
918
920
  --rating-star: var(--colors-state-warning);
@@ -1007,5 +1009,8 @@
1007
1009
  --image-crop-dialog-selection: var(--text-on-cover);
1008
1010
  --image-crop-dialog-handle: var(--text-on-cover);
1009
1011
  --image-crop-dialog-stage-background: var(--surface-muted);
1012
+ --loading-overlay-scrim: var(--surface-scrim);
1013
+ --loading-overlay-background: var(--surface-card);
1014
+ --loading-overlay-text: var(--text-body);
1010
1015
  }
1011
1016
  }