ionbase-ui 0.3.0 → 0.5.0

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.
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ export type FullCardAlignment = 'right' | 'left';
3
+ /**
4
+ * Which heading element the headline renders as.
5
+ *
6
+ * `h1` is deliberately absent: a full card is a section within a page, never
7
+ * the page's own title.
8
+ */
9
+ export type FullCardHeadingLevel = 2 | 3 | 4 | 5 | 6;
10
+ export interface FullCardProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
11
+ /**
12
+ * Which side the media panel sits on. Matches Figma's `Alignment` variant,
13
+ * whose `Right` / `Left` name the media, not the text.
14
+ */
15
+ alignment?: FullCardAlignment;
16
+ /** Figma's `Eyebrow` slot — a Badge in the design, but any node works. */
17
+ eyebrow?: React.ReactNode;
18
+ /** Figma's `Headline`. Required: a card with no headline has no accessible name. */
19
+ headline: React.ReactNode;
20
+ /** Figma's `Description`. */
21
+ description?: React.ReactNode;
22
+ /** Figma's `Actions` slot — a secondary Button in the design. */
23
+ actions?: React.ReactNode;
24
+ /**
25
+ * Figma's `Media` slot. Rendered inside the framed screen holder, so pass
26
+ * the screenshot or embed itself, not the frame.
27
+ */
28
+ media?: React.ReactNode;
29
+ /**
30
+ * Heading element for the headline. Defaults to `h3`, matching the `Type/H3`
31
+ * text style Figma applies — but the level is a document-outline decision
32
+ * only the page knows, so it is a prop rather than a constant.
33
+ */
34
+ headingLevel?: FullCardHeadingLevel;
35
+ children?: React.ReactNode;
36
+ }
37
+ /**
38
+ * Full Card — the full-bleed case study row from Figma `Full Card` (592:857).
39
+ *
40
+ * A text column beside a framed media panel, split down the middle, mirrored
41
+ * by the `Alignment` variant.
42
+ *
43
+ * The split holds from 1080 and stacks below it, media above content in BOTH
44
+ * alignments — `Alignment` names a horizontal side and
45
+ * stops meaning anything once there is one column, so it does not get to
46
+ * decide the vertical order too. Size is a media query, not a prop, the same
47
+ * call Header makes about Device.
48
+ *
49
+ * WHY THE `show*` BOOLEANS ARE GONE
50
+ *
51
+ * Figma carries `Show Eyebrow`, `Show Description` and `Show Actions` beside
52
+ * the slots they gate, because a Figma component instance always holds every
53
+ * layer and needs a switch to hide one. React has no such constraint — an
54
+ * absent prop is the switch. Badge made the same call with `Show Dot`, and
55
+ * keeping both would have let `showActions` and `actions` disagree.
56
+ *
57
+ * `headline` is required, and it is the only required prop. The heading is
58
+ * what makes this a section rather than a decorated div, and every other part
59
+ * of the card is optional in Figma too.
60
+ *
61
+ * No `'use client'`: nothing here is stateful or interactive. The interactive
62
+ * parts arrive through `actions`, and they carry their own boundary.
63
+ */
64
+ export declare const FullCard: React.ForwardRefExoticComponent<FullCardProps & React.RefAttributes<HTMLElement>>;
65
+ //# sourceMappingURL=FullCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FullCard.d.ts","sourceRoot":"","sources":["../../src/components/FullCard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAqB,MAAM,OAAO,CAAC;AAE1C,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,MAAM,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAErD,MAAM,WAAW,aAAc,SAAQ,IAAI,CACzC,KAAK,CAAC,cAAc,CAAC,WAAW,CAAC,EACjC,OAAO,CACR;IACC;;;OAGG;IACH,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,0EAA0E;IAC1E,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,oFAAoF;IACpF,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,6BAA6B;IAC7B,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,iEAAiE;IACjE,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,QAAQ,mFAgEpB,CAAC"}
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ /**
4
+ * Full Card — the full-bleed case study row from Figma `Full Card` (592:857).
5
+ *
6
+ * A text column beside a framed media panel, split down the middle, mirrored
7
+ * by the `Alignment` variant.
8
+ *
9
+ * The split holds from 1080 and stacks below it, media above content in BOTH
10
+ * alignments — `Alignment` names a horizontal side and
11
+ * stops meaning anything once there is one column, so it does not get to
12
+ * decide the vertical order too. Size is a media query, not a prop, the same
13
+ * call Header makes about Device.
14
+ *
15
+ * WHY THE `show*` BOOLEANS ARE GONE
16
+ *
17
+ * Figma carries `Show Eyebrow`, `Show Description` and `Show Actions` beside
18
+ * the slots they gate, because a Figma component instance always holds every
19
+ * layer and needs a switch to hide one. React has no such constraint — an
20
+ * absent prop is the switch. Badge made the same call with `Show Dot`, and
21
+ * keeping both would have let `showActions` and `actions` disagree.
22
+ *
23
+ * `headline` is required, and it is the only required prop. The heading is
24
+ * what makes this a section rather than a decorated div, and every other part
25
+ * of the card is optional in Figma too.
26
+ *
27
+ * No `'use client'`: nothing here is stateful or interactive. The interactive
28
+ * parts arrive through `actions`, and they carry their own boundary.
29
+ */
30
+ export const FullCard = forwardRef(({ alignment = 'right', eyebrow, headline, description, actions, media, headingLevel = 3, className, children, ...rest }, ref) => {
31
+ const Heading = `h${headingLevel}`;
32
+ return (_jsxs("section", { ...rest, ref: ref, className: [
33
+ 'ion-full-card',
34
+ `ion-full-card--media-${alignment}`,
35
+ className,
36
+ ]
37
+ .filter(Boolean)
38
+ .join(' '), children: [_jsx("div", { className: "ion-full-card__content", children: _jsxs("div", { className: "ion-full-card__body", children: [eyebrow && _jsx("div", { className: "ion-full-card__eyebrow", children: eyebrow }), _jsx(Heading, { className: "ion-full-card__headline", children: headline }), description && (_jsx("p", { className: "ion-full-card__description", children: description })), actions && _jsx("div", { className: "ion-full-card__actions", children: actions }), children] }) }), _jsx("div", { className: "ion-full-card__media", children: _jsx("div", { className: "ion-full-card__media-mat", children: _jsx("div", { className: "ion-full-card__media-frame", children: _jsx("div", { className: "ion-full-card__media-screen", children: media }) }) }) })] }));
39
+ });
40
+ FullCard.displayName = 'FullCard';
41
+ //# sourceMappingURL=FullCard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FullCard.js","sourceRoot":"","sources":["../../src/components/FullCard.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AA2C1C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,UAAU,CAChC,CACE,EACE,SAAS,GAAG,OAAO,EACnB,OAAO,EACP,QAAQ,EACR,WAAW,EACX,OAAO,EACP,KAAK,EACL,YAAY,GAAG,CAAC,EAChB,SAAS,EACT,QAAQ,EACR,GAAG,IAAI,EACR,EACD,GAAG,EACH,EAAE;IACF,MAAM,OAAO,GAAG,IAAI,YAAY,EAAW,CAAC;IAE5C,OAAO,CACL,sBACM,IAAI,EACR,GAAG,EAAE,GAAG,EACR,SAAS,EAAE;YACT,eAAe;YACf,wBAAwB,SAAS,EAAE;YACnC,SAAS;SACV;aACE,MAAM,CAAC,OAAO,CAAC;aACf,IAAI,CAAC,GAAG,CAAC,aAQZ,cAAK,SAAS,EAAC,wBAAwB,YACrC,eAAK,SAAS,EAAC,qBAAqB,aACjC,OAAO,IAAI,cAAK,SAAS,EAAC,wBAAwB,YAAE,OAAO,GAAO,EACnE,KAAC,OAAO,IAAC,SAAS,EAAC,yBAAyB,YAAE,QAAQ,GAAW,EAChE,WAAW,IAAI,CACd,YAAG,SAAS,EAAC,4BAA4B,YAAE,WAAW,GAAK,CAC5D,EACA,OAAO,IAAI,cAAK,SAAS,EAAC,wBAAwB,YAAE,OAAO,GAAO,EAClE,QAAQ,IACL,GACF,EAQN,cAAK,SAAS,EAAC,sBAAsB,YACnC,cAAK,SAAS,EAAC,0BAA0B,YACvC,cAAK,SAAS,EAAC,4BAA4B,YACzC,cAAK,SAAS,EAAC,6BAA6B,YAAE,KAAK,GAAO,GACtD,GACF,GACF,IACE,CACX,CAAC;AACJ,CAAC,CACF,CAAC;AAEF,QAAQ,CAAC,WAAW,GAAG,UAAU,CAAC"}
@@ -30,4 +30,6 @@ export { Table, TableHead, TableBody, TableRow, TableCell } from './Table.js';
30
30
  export type { TableProps, TableDensity, TableRowProps, TableRowSelection, TableCellProps, TableCellAlign, TableCellScope, } from './Table.js';
31
31
  export { ScrollProgress } from './ScrollProgress.js';
32
32
  export type { ScrollProgressProps, ScrollProgressSection, } from './ScrollProgress.js';
33
+ export { FullCard } from './FullCard.js';
34
+ export type { FullCardProps, FullCardAlignment, FullCardHeadingLevel, } from './FullCard.js';
33
35
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EACV,aAAa,EACb,YAAY,EACZ,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC/C,YAAY,EACV,UAAU,EACV,eAAe,EACf,SAAS,EACT,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC3C,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAClD,YAAY,EACV,WAAW,EACX,gBAAgB,EAChB,UAAU,EACV,WAAW,GACZ,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC3C,YAAY,EACV,SAAS,EACT,aAAa,EACb,QAAQ,EACR,YAAY,GACb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC9E,YAAY,EACV,UAAU,EACV,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,YAAY,EACV,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC/D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EACV,aAAa,EACb,YAAY,EACZ,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC/C,YAAY,EACV,UAAU,EACV,eAAe,EACf,SAAS,EACT,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC3C,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAClD,YAAY,EACV,WAAW,EACX,gBAAgB,EAChB,UAAU,EACV,WAAW,GACZ,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC3C,YAAY,EACV,SAAS,EACT,aAAa,EACb,QAAQ,EACR,YAAY,GACb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC9E,YAAY,EACV,UAAU,EACV,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,YAAY,EACV,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,eAAe,CAAC"}
@@ -14,4 +14,5 @@ export { Divider } from './Divider.js';
14
14
  export { NavItem } from './NavItem.js';
15
15
  export { Table, TableHead, TableBody, TableRow, TableCell } from './Table.js';
16
16
  export { ScrollProgress } from './ScrollProgress.js';
17
+ export { FullCard } from './FullCard.js';
17
18
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAMzC,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAO/C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAOlD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAO3C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAU9E,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAMzC,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAO/C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAOlD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAO3C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAU9E,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAKrD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
@@ -89,10 +89,34 @@
89
89
  border-style: solid;
90
90
  border-width: var(--border-width-default);
91
91
  border-radius: var(--radius-md);
92
+
93
+ /*
94
+ * `base` + `ease-out`, from the shared motion ladder in motion.css.
95
+ *
96
+ * This was `150ms cubic-bezier(0.4, 0, 0.2, 1)`, hardcoded here and in eleven
97
+ * other stylesheets. Both halves were wrong for a state change: 150ms is
98
+ * short enough to read as a jump rather than a change, and that curve is
99
+ * symmetric — its slow tail means a colour appears to arrive late and then
100
+ * snap. `ease-out` leaves immediately and decelerates in, so the same change
101
+ * reads as settling.
102
+ */
92
103
  transition:
93
- background-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
94
- border-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
95
- box-shadow 150ms cubic-bezier(0.4, 0, 0.2, 1);
104
+ background-color var(--ion-duration-base) var(--ion-ease-out),
105
+ border-color var(--ion-duration-base) var(--ion-ease-out),
106
+ box-shadow var(--ion-duration-base) var(--ion-ease-out);
107
+ }
108
+
109
+ /*
110
+ * Press drops to `fast`.
111
+ *
112
+ * A press must resolve while the pointer is still down or the feedback reads as
113
+ * lag rather than as a press — it is the one interaction the user is actively
114
+ * waiting on. Release still returns at `base`, because the rule only overrides
115
+ * the duration while the pressed state holds.
116
+ */
117
+ .ion-button:active,
118
+ .ion-button[data-pressed='true'] {
119
+ transition-duration: var(--ion-duration-fast);
96
120
  }
97
121
 
98
122
  /* Size modifiers — radius, border-width and font-weight are size-invariant. */
@@ -204,12 +228,12 @@
204
228
 
205
229
  @media (hover: hover) {
206
230
  .ion-button--primary-neutral:hover {
207
- background-color: var(--surface-inverse);
231
+ background-color: var(--surface-inverse-subtle);
208
232
  }
209
233
  }
210
234
 
211
235
  .ion-button--primary-neutral[data-hovered='true'] {
212
- background-color: var(--surface-inverse);
236
+ background-color: var(--surface-inverse-subtle);
213
237
  }
214
238
 
215
239
  .ion-button--primary-neutral:active,
@@ -229,16 +253,20 @@
229
253
  border-color: var(--border-strong);
230
254
  }
231
255
 
256
+ /*
257
+ * The border does NOT move on hover — Figma binds `border/strong` on Default,
258
+ * Hover and Focus alike, and only steps to `border/stronger` on Pressed. The
259
+ * hover step was previously reproduced here and is a difference the current
260
+ * design does not draw.
261
+ */
232
262
  @media (hover: hover) {
233
263
  .ion-button--secondary:hover {
234
264
  background-color: var(--surface-page);
235
- border-color: var(--border-stronger);
236
265
  }
237
266
  }
238
267
 
239
268
  .ion-button--secondary[data-hovered='true'] {
240
269
  background-color: var(--surface-page);
241
- border-color: var(--border-stronger);
242
270
  }
243
271
 
244
272
  .ion-button--secondary:active,
@@ -246,6 +274,7 @@
246
274
  --ion-button-elevation: var(--ion-button-pressed);
247
275
 
248
276
  background-color: var(--surface-muted);
277
+ border-color: var(--border-stronger);
249
278
  }
250
279
 
251
280
  /* Tertiary Variant — intentionally flat; a ghost button has no surface to raise. */
@@ -124,8 +124,8 @@
124
124
  (var(--ion-checkbox-line-height) - var(--ion-checkbox-size)) / 2
125
125
  );
126
126
  transition:
127
- background-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
128
- border-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
127
+ background-color var(--ion-duration-base) var(--ion-ease-out),
128
+ border-color var(--ion-duration-base) var(--ion-ease-out);
129
129
  }
130
130
 
131
131
  .ion-checkbox__mark {
@@ -0,0 +1,287 @@
1
+ /*
2
+ * Full Card
3
+ *
4
+ * Measured from Figma `Full Card` (592:857) — a component set with one axis,
5
+ * `Alignment`, whose `Right` (590:189) and `Left` (592:858) variants mirror the
6
+ * media panel across the card.
7
+ *
8
+ * content column padding 0 / 48, centred, hugs half the width
9
+ * media column surface/page, 1px rule facing the content, padding-top 48
10
+ * media mat surface/sunken, 8px inset, radius/6xl on the outer top corner
11
+ * media frame surface/default, 1px rule on three sides, radius/5xl
12
+ *
13
+ * WHAT WAS NOT COPIED FROM FIGMA, AND WHY
14
+ *
15
+ * 1. The 366px height and 1296px width are canvas measurements, not
16
+ * constraints. 366 is what the parts add up to — 48 (column) + 8 (mat) +
17
+ * 310 (screen) — so it falls out of the box model and stating it again
18
+ * would only pin the card to one media size. 1296 is the desktop container
19
+ * width, which belongs to the page, not the card.
20
+ *
21
+ * 2. The description container's gap reads 9px in Figma. There is no 9 on the
22
+ * spacing ladder and no token that resolves to one, so it is a nudge rather
23
+ * than a decision; this uses `spacing/8`. Change it in Figma if the 9 was
24
+ * meant, and it becomes a real token here.
25
+ *
26
+ * 3. The left variant carries its media rule on the media column's LEFT edge —
27
+ * i.e. the card's outer edge — while the right variant carries it on the
28
+ * edge facing the text. Only one of those is a divider; the other is a
29
+ * stray line at the boundary of the card. Read as a mirroring oversight in
30
+ * Figma, so both variants draw the rule on the inner edge here. Worth
31
+ * fixing in the file, since the two disagree today.
32
+ *
33
+ * 4. The left variant's media column also hard-codes a 48px top padding where
34
+ * the right variant binds `spacing/48`. Same oversight, same resolution:
35
+ * the token, both times.
36
+ *
37
+ * 5. Figma has no narrow variant of this card, and a 1296-wide two-column row
38
+ * is unusable below its own width. The stacked layout is repo-owned, on the
39
+ * same reasoning as Header's device media queries: the browser knows the
40
+ * viewport, so the caller should not have to say.
41
+ *
42
+ * The split holds from 1080 — a chosen number, not a Breakpoint collection
43
+ * container width, so it is stated here rather than in the token pipeline.
44
+ * The two halves are even only while there is room for it: below roughly
45
+ * 1190 the media column holds at the panel plus its gutter and the text
46
+ * column takes what is left, bottoming out near 375px of text at 1080. The
47
+ * 896 query below changes no layout at all — only the text gutter.
48
+ *
49
+ * STACKED PUTS THE MEDIA ON TOP, IN BOTH ALIGNMENTS
50
+ *
51
+ * `column-reverse`, so the DOM stays content-then-media while the media renders
52
+ * above the text. Two reasons it is the media rather than the headline:
53
+ *
54
+ * - `Alignment` stops meaning anything once there is only one column. Letting
55
+ * it decide the vertical order would turn one axis into two, and a card
56
+ * whose text sits above the panel on some rows and below it on others reads
57
+ * as a bug rather than a variant.
58
+ * - The panel is the thing the card is about. Stacked, it is also the only
59
+ * part with a fixed height, so putting it second would push it below the
60
+ * fold on every narrow viewport.
61
+ */
62
+ .ion-full-card {
63
+ --ion-full-card-padding-x: var(--spacing-24);
64
+ --ion-full-card-padding-y: var(--spacing-32);
65
+ --ion-full-card-gap: var(--spacing-8);
66
+ --ion-full-card-media-width: 562px;
67
+
68
+ /*
69
+ * The least space allowed between the centre rule and the media panel.
70
+ * Figma's is 86 at the design width (648 column − 562 panel), which is not on
71
+ * the spacing ladder and is a consequence of the 50/50 split rather than a
72
+ * decision. `spacing/48` is the floor, and it is the same 48 the panel is
73
+ * already inset from the top of the card — so when the row is too narrow for
74
+ * an even split, the panel keeps a gutter that matches its own top edge.
75
+ */
76
+ --ion-full-card-media-gutter: var(--spacing-48);
77
+
78
+ /*
79
+ * Placeholder geometry: the height of Figma's empty `Screen Container`. It
80
+ * is a floor, not a size — real media in the slot sets the height and this
81
+ * stops an empty card collapsing. Override per instance if the media has a
82
+ * known aspect ratio.
83
+ */
84
+ --ion-full-card-media-min-height: 310px;
85
+
86
+ display: flex;
87
+ flex-direction: column-reverse;
88
+ width: 100%;
89
+ border-top: var(--border-width-default) solid var(--border-default);
90
+ border-bottom: var(--border-width-default) solid var(--border-default);
91
+ font-family: var(--font-family-sans);
92
+ }
93
+
94
+ /*
95
+ * `flex: 0 0 auto` while stacked, not the `1 1 0` the split uses. In a column
96
+ * flex container a zero basis is a zero HEIGHT that then grows to fill — which
97
+ * is the two halves splitting the viewport instead of sizing to their content.
98
+ * The split re-states `1 1 0` at Desktop, where the axis is horizontal and a
99
+ * zero basis is what makes the halves equal regardless of text length.
100
+ */
101
+ .ion-full-card__content {
102
+ display: flex;
103
+ flex-direction: column;
104
+ justify-content: center;
105
+ flex: 0 0 auto;
106
+ min-width: 0;
107
+ padding: var(--ion-full-card-padding-y) var(--ion-full-card-padding-x);
108
+ overflow: hidden;
109
+ }
110
+
111
+ .ion-full-card__body {
112
+ display: flex;
113
+ flex-direction: column;
114
+ align-items: flex-start;
115
+ gap: var(--ion-full-card-gap);
116
+ width: 100%;
117
+ }
118
+
119
+ .ion-full-card__eyebrow,
120
+ .ion-full-card__actions {
121
+ display: flex;
122
+ align-items: center;
123
+ gap: var(--spacing-8);
124
+ flex-wrap: wrap;
125
+ }
126
+
127
+ /*
128
+ * The type styles are restated rather than delegated to `.ion-text-h3` /
129
+ * `.ion-text-body-sm`, so the card renders correctly whatever classes the
130
+ * caller puts on the headline. Both sets read the same breakpoint-scoped
131
+ * `type/*` tokens, so they resize together.
132
+ */
133
+ .ion-full-card__headline {
134
+ margin: 0;
135
+ font-family: var(--font-family-sans);
136
+ font-weight: var(--font-weight-semibold);
137
+ font-size: var(--type-h3);
138
+ line-height: var(--type-h3-line-height);
139
+ color: var(--text-default);
140
+ }
141
+
142
+ .ion-full-card__description {
143
+ margin: 0;
144
+ font-family: var(--font-family-sans);
145
+ font-weight: var(--font-weight-regular);
146
+ font-size: var(--type-body-sm);
147
+ line-height: var(--type-body-sm-line-height);
148
+ color: var(--text-secondary);
149
+ }
150
+
151
+ .ion-full-card__media {
152
+ display: flex;
153
+ flex-direction: column;
154
+ justify-content: flex-end;
155
+ align-items: stretch;
156
+ flex: 0 0 auto;
157
+ min-width: 0;
158
+ padding-top: var(--spacing-48);
159
+
160
+ /*
161
+ * Stacked, the panel is inset from the card's left and right edges too, not
162
+ * just the top — and by the text's own gutter, so the panel and the headline
163
+ * share an edge instead of sitting on two arbitrary ones. In the split the
164
+ * panel deliberately bleeds off the outer edge, so this resets to zero there.
165
+ */
166
+ padding-inline: var(--ion-full-card-padding-x);
167
+ background-color: var(--surface-page);
168
+
169
+ /*
170
+ * The stacked analogue of the vertical rule between the two halves. The
171
+ * frame deliberately has no bottom border, so without this the white screen
172
+ * would run straight into the text with nothing separating them.
173
+ */
174
+ border-bottom: var(--border-width-default) solid var(--border-default);
175
+ overflow: hidden;
176
+ }
177
+
178
+ .ion-full-card__media-mat {
179
+ width: 100%;
180
+ padding-top: var(--spacing-8);
181
+ padding-inline: var(--spacing-8);
182
+ background-color: var(--surface-sunken);
183
+ border-start-start-radius: var(--radius-6xl);
184
+ border-start-end-radius: var(--radius-6xl);
185
+ overflow: hidden;
186
+ }
187
+
188
+ /*
189
+ * Three sides only. The card is a full-bleed band and the panel runs off its
190
+ * bottom edge, so a fourth rule would draw a line the design does not have.
191
+ */
192
+ .ion-full-card__media-frame {
193
+ width: 100%;
194
+ border: var(--border-width-default) solid var(--border-default);
195
+ border-bottom: none;
196
+ border-start-start-radius: var(--radius-5xl);
197
+ border-start-end-radius: var(--radius-5xl);
198
+ overflow: hidden;
199
+ }
200
+
201
+ .ion-full-card__media-screen {
202
+ width: 100%;
203
+ min-height: var(--ion-full-card-media-min-height);
204
+ background-color: var(--surface-default);
205
+ }
206
+
207
+ /* Tablet is still stacked; only the text gutter opens up. */
208
+ @media (width >= 896px) {
209
+ .ion-full-card {
210
+ --ion-full-card-padding-x: var(--spacing-32);
211
+ }
212
+ }
213
+
214
+ /*
215
+ * Side by side from 1080 up. Below it the card stacks, media first, the panel
216
+ * takes the text's own gutters and both its top corners round rather than the
217
+ * single outer one.
218
+ */
219
+ @media (width >= 1080px) {
220
+ .ion-full-card {
221
+ --ion-full-card-padding-x: var(--spacing-48);
222
+ --ion-full-card-padding-y: var(--spacing-0);
223
+
224
+ flex-direction: row;
225
+ align-items: stretch;
226
+ }
227
+
228
+ .ion-full-card__content {
229
+ flex: 1 1 0;
230
+ }
231
+
232
+ .ion-full-card--media-left {
233
+ /*
234
+ * Reversed here, not in the DOM: the markup stays headline → actions →
235
+ * media in both alignments so reading and focus order follow the content
236
+ * rather than the mirror.
237
+ */
238
+ flex-direction: row-reverse;
239
+ }
240
+
241
+ /*
242
+ * `1 1 0` for the even split, floored so the panel is never the thing that
243
+ * gives. Below roughly 1190 the halves are narrower than the panel, and
244
+ * without the floor the panel shrank to fill its column — losing the gutter
245
+ * against the centre rule and reading as a full-bleed block right up to the
246
+ * moment the card stacked. The text column absorbs the difference instead:
247
+ * a paragraph reflows, and a panel with no gutter is a different design.
248
+ */
249
+ .ion-full-card__media {
250
+ align-items: flex-end;
251
+ flex: 1 1 0;
252
+ min-width: calc(
253
+ var(--ion-full-card-media-width) + var(--ion-full-card-media-gutter)
254
+ );
255
+ padding-inline: var(--spacing-0);
256
+ border-bottom: none;
257
+ border-inline-start: var(--border-width-default) solid var(--border-default);
258
+ }
259
+
260
+ .ion-full-card--media-left .ion-full-card__media {
261
+ align-items: flex-start;
262
+ border-inline-start: none;
263
+ border-inline-end: var(--border-width-default) solid var(--border-default);
264
+ }
265
+
266
+ .ion-full-card__media-mat {
267
+ width: var(--ion-full-card-media-width);
268
+ max-width: 100%;
269
+ padding-inline: var(--spacing-8) var(--spacing-0);
270
+ border-start-end-radius: var(--radius-none);
271
+ }
272
+
273
+ .ion-full-card--media-left .ion-full-card__media-mat {
274
+ padding-inline: var(--spacing-0) var(--spacing-8);
275
+ border-start-start-radius: var(--radius-none);
276
+ border-start-end-radius: var(--radius-6xl);
277
+ }
278
+
279
+ .ion-full-card__media-frame {
280
+ border-start-end-radius: var(--radius-none);
281
+ }
282
+
283
+ .ion-full-card--media-left .ion-full-card__media-frame {
284
+ border-start-start-radius: var(--radius-none);
285
+ border-start-end-radius: var(--radius-5xl);
286
+ }
287
+ }
@@ -36,6 +36,7 @@
36
36
  @import url('./divider.css');
37
37
  @import url('./nav-item.css');
38
38
  @import url('./scroll-progress.css');
39
+ @import url('./full-card.css');
39
40
 
40
41
  /*
41
42
  * Every IonBase element is border-box.
@@ -63,8 +63,8 @@
63
63
  font-size: var(--ion-input-font-size);
64
64
  line-height: var(--ion-input-line-height);
65
65
  transition:
66
- border-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
67
- background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
66
+ border-color var(--ion-duration-base) var(--ion-ease-out),
67
+ background-color var(--ion-duration-base) var(--ion-ease-out);
68
68
  }
69
69
 
70
70
  /*
@@ -54,7 +54,7 @@
54
54
  width: 100%;
55
55
  cursor: pointer;
56
56
  user-select: none;
57
- transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
57
+ transition: background-color var(--ion-duration-base) var(--ion-ease-out);
58
58
  }
59
59
 
60
60
  .ion-menu__icon,
@@ -37,7 +37,7 @@
37
37
  text-decoration: none;
38
38
  cursor: pointer;
39
39
  user-select: none;
40
- transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1);
40
+ transition: color var(--ion-duration-base) var(--ion-ease-out);
41
41
  }
42
42
 
43
43
  .ion-nav-item__icon,
@@ -47,7 +47,7 @@
47
47
  justify-content: center;
48
48
  flex-shrink: 0;
49
49
  color: var(--icon-tertiary);
50
- transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1);
50
+ transition: color var(--ion-duration-base) var(--ion-ease-out);
51
51
  }
52
52
 
53
53
  .ion-nav-item__icon svg,
@@ -96,8 +96,8 @@
96
96
  /* Centred on the label's first line — see checkbox.css. */
97
97
  margin-top: calc((var(--ion-radio-line-height) - var(--ion-radio-size)) / 2);
98
98
  transition:
99
- background-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
100
- border-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
99
+ background-color var(--ion-duration-base) var(--ion-ease-out),
100
+ border-color var(--ion-duration-base) var(--ion-ease-out);
101
101
  }
102
102
 
103
103
  .ion-radio__dot {
@@ -29,8 +29,10 @@
29
29
  *
30
30
  * REVEAL TRANSITION IS DELIBERATELY PLAIN
31
31
  *
32
- * Opacity and an 8px slide, the same 150ms/cubic-bezier(0.4,0,0.2,1) language
33
- * every hover state in the system already uses. The deeper choreography this
32
+ * Opacity and an 8px slide on the shared motion ladder — `--ion-duration-base`
33
+ * and `--ion-ease-out`, the same language every hover state in the system
34
+ * already uses. It reads as a reveal rather than travel, so it stays on `base`
35
+ * rather than taking the `slow` rung. The deeper choreography this
34
36
  * component could eventually justify — a staggered reveal per row, easing
35
37
  * that matches the rail's own scale — is future work, tracked in
36
38
  * docs/motion-system.md, not invented here ahead of that decision.
@@ -62,7 +64,7 @@
62
64
  font-family: var(--font-family-sans);
63
65
  font-size: var(--type-body-sm);
64
66
  line-height: var(--type-body-sm-line-height);
65
- transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1);
67
+ transition: color var(--ion-duration-base) var(--ion-ease-out);
66
68
  }
67
69
 
68
70
  .ion-scroll-progress__rail {
@@ -76,7 +78,7 @@
76
78
  width: var(--spacing-32);
77
79
  height: var(--border-width-thick);
78
80
  background-color: var(--border-strong);
79
- transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
81
+ transition: background-color var(--ion-duration-base) var(--ion-ease-out);
80
82
  }
81
83
 
82
84
  .ion-scroll-progress__tick--active {
@@ -118,9 +120,9 @@
118
120
  pointer-events: none;
119
121
  transform: translateY(-50%) translateX(-4px);
120
122
  transition:
121
- opacity 150ms cubic-bezier(0.4, 0, 0.2, 1),
122
- transform 150ms cubic-bezier(0.4, 0, 0.2, 1),
123
- visibility 150ms;
123
+ opacity var(--ion-duration-base) var(--ion-ease-out),
124
+ transform var(--ion-duration-base) var(--ion-ease-out),
125
+ visibility var(--ion-duration-base);
124
126
  }
125
127
 
126
128
  .ion-scroll-progress__panel[data-open='true'] {
@@ -145,8 +147,8 @@
145
147
  text-align: left;
146
148
  cursor: pointer;
147
149
  transition:
148
- background-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
149
- border-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
150
+ background-color var(--ion-duration-base) var(--ion-ease-out),
151
+ border-color var(--ion-duration-base) var(--ion-ease-out);
150
152
  }
151
153
 
152
154
  @media (hover: hover) {
@@ -47,8 +47,8 @@
47
47
  font-size: var(--ion-select-font-size);
48
48
  line-height: var(--ion-select-line-height);
49
49
  transition:
50
- border-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
51
- background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
50
+ border-color var(--ion-duration-base) var(--ion-ease-out),
51
+ background-color var(--ion-duration-base) var(--ion-ease-out);
52
52
  }
53
53
 
54
54
  /* Small shares Medium's padding — see input.css. */
@@ -107,7 +107,7 @@
107
107
 
108
108
  .ion-table tr {
109
109
  background-color: var(--surface-default);
110
- transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
110
+ transition: background-color var(--ion-duration-base) var(--ion-ease-out);
111
111
  }
112
112
 
113
113
  .ion-table tbody tr {
@@ -80,9 +80,9 @@
80
80
  cursor: pointer;
81
81
  user-select: none;
82
82
  transition:
83
- color 150ms cubic-bezier(0.4, 0, 0.2, 1),
84
- background-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
85
- border-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
83
+ color var(--ion-duration-base) var(--ion-ease-out),
84
+ background-color var(--ion-duration-base) var(--ion-ease-out),
85
+ border-color var(--ion-duration-base) var(--ion-ease-out);
86
86
  }
87
87
 
88
88
  .ion-tabs__item--sm {
@@ -115,8 +115,8 @@
115
115
  (var(--ion-toggle-line-height) - var(--ion-toggle-track-h)) / 2
116
116
  );
117
117
  transition:
118
- background-color 150ms cubic-bezier(0.4, 0, 0.2, 1),
119
- border-color 150ms cubic-bezier(0.4, 0, 0.2, 1);
118
+ background-color var(--ion-duration-base) var(--ion-ease-out),
119
+ border-color var(--ion-duration-base) var(--ion-ease-out);
120
120
  }
121
121
 
122
122
  .ion-toggle__thumb {
@@ -127,7 +127,14 @@
127
127
 
128
128
  /* Cast, not bevelled — and the same in both states, so it is unconditional. */
129
129
  box-shadow: var(--ion-shadow-shadow-xs);
130
- transition: margin 150ms cubic-bezier(0.4, 0, 0.2, 1);
130
+
131
+ /*
132
+ * `ease-in-out`, not the `ease-out` every colour change uses. The knob is the
133
+ * one thing in the system that travels the same path in both directions, so
134
+ * a symmetric curve is what makes on and off feel like the same gesture
135
+ * reversed. Everything else enters a state and stays there.
136
+ */
137
+ transition: margin var(--ion-duration-base) var(--ion-ease-in-out);
131
138
  }
132
139
 
133
140
  .ion-toggle__label {
@@ -288,7 +288,7 @@
288
288
  --surface-raised: #ffffff;
289
289
  --surface-overlay: #ffffff;
290
290
  --surface-scrim: rgba(0, 0, 0, 0.4);
291
- --surface-inverse: #131923;
291
+ --surface-inverse: #1d2735;
292
292
  --surface-inverse-subtle: #384252;
293
293
  --surface-disabled: #f0f2f4;
294
294
  --surface-placeholder: #dde0e4;
@@ -34,25 +34,25 @@
34
34
  0px 8px 8px -4px rgb(0 0 0 / 4%);
35
35
 
36
36
  --ion-shadow-raised-lifted-xs:
37
- inset 0px -2px 1px 0px rgb(0 0 0 / 40%),
37
+ inset 0px -2px 1px 0px rgb(0 0 0 / 25%),
38
38
  inset 0px 2px 1px 0px rgb(255 255 255 / 20%),
39
39
  0px 1px 1px 0px rgb(7 10 13 / 6%),
40
40
  0px 1px 2px 0px rgb(7 10 13 / 4%);
41
41
 
42
42
  --ion-shadow-raised-lifted-sm:
43
- inset 0px -3px 1px 0px rgb(0 0 0 / 30%),
43
+ inset 0px -3px 1px 0px rgb(0 0 0 / 25%),
44
44
  inset 0px 3px 1px 0px rgb(255 255 255 / 20%),
45
45
  0px 1px 2px 0px rgb(7 10 13 / 8%),
46
46
  0px 2px 4px 0px rgb(7 10 13 / 5%);
47
47
 
48
48
  --ion-shadow-raised-lifted-lg:
49
- inset 0px -4px 2px 0px rgb(0 0 0 / 40%),
49
+ inset 0px -4px 2px 0px rgb(0 0 0 / 25%),
50
50
  inset 0px 4px 2px 0px rgb(255 255 255 / 20%),
51
51
  0px 1px 1px 0px rgb(7 10 13 / 32%),
52
52
  0px 1px 2px 0px rgb(7 10 13 / 16%);
53
53
 
54
54
  --ion-shadow-raised-lifted-xl:
55
- inset 0px -6px 2px 0px rgb(0 0 0 / 35%),
55
+ inset 0px -6px 2px 0px rgb(0 0 0 / 25%),
56
56
  inset 0px 6px 2px 0px rgb(255 255 255 / 20%),
57
57
  0px 1px 2px 0px rgb(7 10 13 / 30%),
58
58
  0px 2px 4px 0px rgb(7 10 13 / 14%);
@@ -74,19 +74,19 @@
74
74
  0px 2px 6px 0px rgb(7 10 13 / 20%);
75
75
 
76
76
  --ion-shadow-raised-flush-xs:
77
- inset 0px -2px 1px 0px rgb(0 0 0 / 40%),
77
+ inset 0px -2px 1px 0px rgb(0 0 0 / 25%),
78
78
  inset 0px 2px 1px 0px rgb(255 255 255 / 20%);
79
79
 
80
80
  --ion-shadow-raised-flush-sm:
81
- inset 0px -3px 1px 0px rgb(0 0 0 / 30%),
81
+ inset 0px -3px 1px 0px rgb(0 0 0 / 25%),
82
82
  inset 0px 3px 1px 0px rgb(255 255 255 / 20%);
83
83
 
84
84
  --ion-shadow-raised-flush-lg:
85
- inset 0px -4px 2px 0px rgb(0 0 0 / 40%),
85
+ inset 0px -4px 2px 0px rgb(0 0 0 / 25%),
86
86
  inset 0px 4px 2px 0px rgb(255 255 255 / 20%);
87
87
 
88
88
  --ion-shadow-raised-flush-xl:
89
- inset 0px -6px 2px 0px rgb(0 0 0 / 35%),
89
+ inset 0px -6px 2px 0px rgb(0 0 0 / 25%),
90
90
  inset 0px 6px 2px 0px rgb(255 255 255 / 20%);
91
91
 
92
92
  --ion-shadow-inset-lifted-xs:
@@ -4,3 +4,4 @@
4
4
  @import "./breakpoint-mobile.css";
5
5
  @import "./typography.css";
6
6
  @import "./elevation.css";
7
+ @import "./motion.css";
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Generated from motion.json. Do not edit.
3
+ * Regenerate with `pnpm --filter @ionbase-ui/tokens build`.
4
+ *
5
+ * Source: motion.json (repo-owned — NOT a Figma export)
6
+ *
7
+ * These are NOT tokens. Figma expresses motion as prototype reactions rather
8
+ * than variables, so there is nothing for them to alias; the values are
9
+ * committed and generated instead. The `--ion-` prefix marks that, the same
10
+ * way it does for `--ion-shadow-*`.
11
+ *
12
+ * A LADDER, NOT RECIPES. Pick a rung per component. There is deliberately no
13
+ * `--ion-duration-hover`: that is indexed by usage, so it needs a new name
14
+ * per usage pattern.
15
+ *
16
+ * Reduced motion is handled globally in the package's index.css, not here —
17
+ * one block covering every `ion-*` rule, rather than a media query per value.
18
+ */
19
+
20
+ :root {
21
+ /* Press and release. Must resolve before the finger leaves the button, or */
22
+ /* the feedback reads as lag rather than as a press. */
23
+ --ion-duration-fast: 120ms;
24
+
25
+ /* The default for any state colour change — hover, focus, selection, */
26
+ /* validation. Replaces a hardcoded 150ms that read as abrupt. */
27
+ --ion-duration-base: 200ms;
28
+
29
+ /* Anything that travels or changes size: panels, overlays, disclosure. */
30
+ /* Distance needs time; a colour does not. */
31
+ --ion-duration-slow: 320ms;
32
+
33
+ /* The default. Leaves immediately and decelerates into the end state, so */
34
+ /* the change reads as settling. Correct for anything entering a state and */
35
+ /* staying there. */
36
+ --ion-ease-out: cubic-bezier(0.2, 0, 0, 1);
37
+
38
+ /* Accelerates and arrives at full velocity. Only for things leaving — */
39
+ /* dismissing an overlay, collapsing a panel. On a state a user is entering */
40
+ /* it reads as a snap. */
41
+ --ion-ease-in: cubic-bezier(0.4, 0, 1, 1);
42
+
43
+ /* Symmetric, for two-way travel where the same element moves back and */
44
+ /* forth — the Toggle knob. This is the curve every stylesheet used for */
45
+ /* everything, which is why state changes felt jerky. */
46
+ --ion-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
47
+
48
+ /* Continuous progress only, where easing would misreport position. Never */
49
+ /* for interaction. */
50
+ --ion-ease-linear: linear;
51
+ }
@@ -1093,7 +1093,7 @@ export declare const tokenValues: {
1093
1093
  readonly Dark: "{alpha.black-60}";
1094
1094
  };
1095
1095
  readonly "surface/inverse": {
1096
- readonly Light: "{neutral.900}";
1096
+ readonly Light: "{neutral.800}";
1097
1097
  readonly Dark: "{base.white}";
1098
1098
  };
1099
1099
  readonly "surface/inverse-subtle": {
@@ -698,7 +698,7 @@ export const tokenValues = {
698
698
  "Dark": "{alpha.black-60}"
699
699
  },
700
700
  "surface/inverse": {
701
- "Light": "{neutral.900}",
701
+ "Light": "{neutral.800}",
702
702
  "Dark": "{base.white}"
703
703
  },
704
704
  "surface/inverse-subtle": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ionbase-ui",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "IonBase Design System — accessible React components, styles and design tokens in one package",
5
5
  "license": "MIT",
6
6
  "type": "module",