ionbase-ui 0.8.0 → 0.11.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.
@@ -26,6 +26,22 @@ export interface InputProps extends AriaTextFieldProps, InputDOMProps {
26
26
  leadingIcon?: React.ReactNode;
27
27
  /** Trailing icon. Figma's `Show Trailing Icon` + `Trailing Icon` swap. */
28
28
  trailingIcon?: React.ReactNode;
29
+ /**
30
+ * A segment butted against the left of the control, sharing its outline —
31
+ * Figma's `Input/Phone` (80:372) country-code block.
32
+ *
33
+ * Distinct from `leadingIcon`, which sits *inside* the box: an addon has its
34
+ * own surface and border and is a sibling of the control, so the two cannot
35
+ * be the same slot. It lives here rather than in `PhoneInput` because the
36
+ * `.ion-field` wrapper has to enclose the addon and the control together —
37
+ * building it outside would put the label above only half the control and
38
+ * break the `aria-labelledby` wiring `useTextField` sets up.
39
+ *
40
+ * Phone is the only addon Figma specifies. This is a slot, not a system:
41
+ * resist growing a trailing counterpart or an addon variant ladder until a
42
+ * design actually calls for one.
43
+ */
44
+ leadingAddon?: React.ReactNode;
29
45
  /**
30
46
  * @deprecated Use `isDisabled`. Accepted as an alias for one minor version.
31
47
  */
@@ -1 +1 @@
1
- {"version":3,"file":"Input.d.ts","sourceRoot":"","sources":["../../src/components/Input.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAkD,MAAM,OAAO,CAAC;AAEvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIrD,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE3C;;;;;;;;;GASG;AACH,KAAK,aAAa,GAAG,IAAI,CACvB,KAAK,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAC3C,MAAM,kBAAkB,GAAG,MAAM,GAAG,gBAAgB,GAAG,UAAU,CAClE,CAAC;AAEF,MAAM,WAAW,UAAW,SAAQ,kBAAkB,EAAE,aAAa;IACnE,8DAA8D;IAC9D,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,GAAG,WAAW,GAAG,OAAO,GAAG,YAAY,CAAC;IAC9E,uEAAuE;IACvE,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,0EAA0E;IAC1E,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC/B;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,KAAK,qFAuHjB,CAAC"}
1
+ {"version":3,"file":"Input.d.ts","sourceRoot":"","sources":["../../src/components/Input.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAkD,MAAM,OAAO,CAAC;AAEvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIrD,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE3C;;;;;;;;;GASG;AACH,KAAK,aAAa,GAAG,IAAI,CACvB,KAAK,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAC3C,MAAM,kBAAkB,GAAG,MAAM,GAAG,gBAAgB,GAAG,UAAU,CAClE,CAAC;AAEF,MAAM,WAAW,UAAW,SAAQ,kBAAkB,EAAE,aAAa;IACnE,8DAA8D;IAC9D,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,GAAG,WAAW,GAAG,OAAO,GAAG,YAAY,CAAC;IAC9E,uEAAuE;IACvE,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,0EAA0E;IAC1E,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC/B;;;;;;;;;;;;;;OAcG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC/B;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,KAAK,qFAmIjB,CAAC"}
@@ -22,7 +22,7 @@ import { resolveDisabled } from './resolve-disabled.js';
22
22
  * expresses; Default, Invalid, Disabled and Read-only are props.
23
23
  */
24
24
  export const Input = forwardRef((props, forwardedRef) => {
25
- const { size = 'md', leadingIcon, trailingIcon, className: customClassName, wrapperClassName, label, description, errorMessage, isDisabled: isDisabledProp, disabled, isReadOnly, isInvalid, ...restProps } = props;
25
+ const { size = 'md', leadingIcon, trailingIcon, leadingAddon, className: customClassName, wrapperClassName, label, description, errorMessage, isDisabled: isDisabledProp, disabled, isReadOnly, isInvalid, ...restProps } = props;
26
26
  const isDisabled = resolveDisabled(isDisabledProp, disabled);
27
27
  const ariaProps = { ...props, isDisabled };
28
28
  /*
@@ -64,19 +64,22 @@ export const Input = forwardRef((props, forwardedRef) => {
64
64
  'data-disabled': isDisabled || undefined,
65
65
  };
66
66
  const box = (_jsxs("div", { ...hoverProps, ...stateAttributes, className: boxClassNames, children: [leadingIcon && (_jsx("span", { className: "ion-input__icon-start", "aria-hidden": "true", children: leadingIcon })), _jsx("input", { ...domProps, ...mergeProps(inputProps, focusProps), ref: domRef, className: "ion-input__field" }), trailingIcon && (_jsx("span", { className: "ion-input__icon-end", "aria-hidden": "true", children: trailingIcon }))] }));
67
+ // The addon and the control share one outline, so they need a row of their
68
+ // own — `.ion-field` is a column and would stack them.
69
+ const control = leadingAddon ? (_jsxs("div", { className: "ion-input-group", children: [leadingAddon, box] })) : (box);
67
70
  // Figma's Error variant recolours the helper text, so the error message
68
71
  // takes the helper's slot rather than stacking below it.
69
72
  const helper = isInvalid && errorMessage ? errorMessage : description;
70
73
  const helperProps = isInvalid && errorMessage ? errorMessageProps : descriptionProps;
71
74
  if (!label && !helper)
72
- return box;
75
+ return control;
73
76
  return (_jsxs("div", { className: [
74
77
  'ion-field',
75
78
  isInvalid ? 'ion-field--error' : '',
76
79
  wrapperClassName || '',
77
80
  ]
78
81
  .filter(Boolean)
79
- .join(' '), children: [label && (_jsx("label", { ...labelProps, className: "ion-field__label", children: label })), box, helper && (_jsx("span", { ...helperProps, className: "ion-field__helper", children: helper }))] }));
82
+ .join(' '), children: [label && (_jsx("label", { ...labelProps, className: "ion-field__label", children: label })), control, helper && (_jsx("span", { ...helperProps, className: "ion-field__helper", children: helper }))] }));
80
83
  });
81
84
  Input.displayName = 'Input';
82
85
  //# sourceMappingURL=Input.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Input.js","sourceRoot":"","sources":["../../src/components/Input.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Input.js","sourceRoot":"","sources":["../../src/components/Input.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAc,EAAE,UAAU,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE9E,OAAO,EAAE,6BAA6B,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AA8DxD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,UAAU,CAC7B,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE;IACtB,MAAM,EACJ,IAAI,GAAG,IAAI,EACX,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,SAAS,EAAE,eAAe,EAC1B,gBAAgB,EAChB,KAAK,EACL,WAAW,EACX,YAAY,EACZ,UAAU,EAAE,cAAc,EAC1B,QAAQ,EACR,UAAU,EACV,SAAS,EACT,GAAG,SAAS,EACb,GAAG,KAAK,CAAC;IAEV,MAAM,UAAU,GAAG,eAAe,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;IAC7D,MAAM,SAAS,GAAG,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,CAAC;IAE3C;;;;;;;;;;;OAWG;IACH,MAAM,QAAQ,GAAG,SAAS,CAAC,SAAS,EAAE,6BAA6B,CAAC,CAAC;IAErE,MAAM,MAAM,GAAG,MAAM,CAAmB,IAAI,CAAC,CAAC;IAC9C,mBAAmB,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAQ,CAAC,CAAC;IAEzD,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,GACnE,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAElC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC;IAC3D,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,GAAG,YAAY,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;IAE3E,MAAM,aAAa,GAAG;QACpB,WAAW;QACX,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE;QACzC,SAAS,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE;QACrC,UAAU,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,EAAE;QACvC,UAAU,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,EAAE;QACvC,eAAe,IAAI,EAAE;KACtB;SACE,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC,CAAC;IAEb,uEAAuE;IACvE,wEAAwE;IACxE,6BAA6B;IAC7B,MAAM,eAAe,GAAG;QACtB,cAAc,EAAE,SAAS,IAAI,SAAS;QACtC,cAAc,EAAE,cAAc,IAAI,SAAS;QAC3C,cAAc,EAAE,SAAS,IAAI,SAAS;QACtC,eAAe,EAAE,UAAU,IAAI,SAAS;QACxC,eAAe,EAAE,UAAU,IAAI,SAAS;KACzC,CAAC;IAEF,MAAM,GAAG,GAAG,CACV,kBAAS,UAAU,KAAM,eAAe,EAAE,SAAS,EAAE,aAAa,aAC/D,WAAW,IAAI,CACd,eAAM,SAAS,EAAC,uBAAuB,iBAAa,MAAM,YACvD,WAAW,GACP,CACR,EACD,mBACM,QAAQ,KACR,UAAU,CAAC,UAAU,EAAE,UAAU,CAAC,EACtC,GAAG,EAAE,MAAM,EACX,SAAS,EAAC,kBAAkB,GAC5B,EACD,YAAY,IAAI,CACf,eAAM,SAAS,EAAC,qBAAqB,iBAAa,MAAM,YACrD,YAAY,GACR,CACR,IACG,CACP,CAAC;IAEF,2EAA2E;IAC3E,uDAAuD;IACvD,MAAM,OAAO,GAAG,YAAY,CAAC,CAAC,CAAC,CAC7B,eAAK,SAAS,EAAC,iBAAiB,aAC7B,YAAY,EACZ,GAAG,IACA,CACP,CAAC,CAAC,CAAC,CACF,GAAG,CACJ,CAAC;IAEF,wEAAwE;IACxE,yDAAyD;IACzD,MAAM,MAAM,GAAG,SAAS,IAAI,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW,CAAC;IACtE,MAAM,WAAW,GACf,SAAS,IAAI,YAAY,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,gBAAgB,CAAC;IAEnE,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM;QAAE,OAAO,OAAO,CAAC;IAEtC,OAAO,CACL,eACE,SAAS,EAAE;YACT,WAAW;YACX,SAAS,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE;YACnC,gBAAgB,IAAI,EAAE;SACvB;aACE,MAAM,CAAC,OAAO,CAAC;aACf,IAAI,CAAC,GAAG,CAAC,aAEX,KAAK,IAAI,CACR,mBAAW,UAAU,EAAE,SAAS,EAAC,kBAAkB,YAChD,KAAK,GACA,CACT,EACA,OAAO,EACP,MAAM,IAAI,CACT,kBAAU,WAAW,EAAE,SAAS,EAAC,mBAAmB,YACjD,MAAyB,GACrB,CACR,IACG,CACP,CAAC;AACJ,CAAC,CACF,CAAC;AAEF,KAAK,CAAC,WAAW,GAAG,OAAO,CAAC"}
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import type { AriaLinkOptions } from 'react-aria';
3
+ export type LinkVariant = 'inline' | 'standalone';
4
+ /**
5
+ * The plain anchor attributes React Aria has no opinion about — `target`,
6
+ * `rel`, `download`, `hreflang` and the rest. `AriaLinkOptions` wins every
7
+ * overlap, the same split `Input` makes against `AriaTextFieldProps`.
8
+ */
9
+ type LinkDOMProps = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof AriaLinkOptions | 'color'>;
10
+ export interface LinkProps extends AriaLinkOptions, LinkDOMProps {
11
+ /**
12
+ * Matches the Figma `Type` variant.
13
+ *
14
+ * `inline` is for links inside body copy and is underlined in every state —
15
+ * a link in a paragraph must not rely on colour alone (WCAG 1.4.1).
16
+ * `standalone` is for links that already read as interactive from their
17
+ * position, and underlines on hover only.
18
+ */
19
+ variant?: LinkVariant;
20
+ /** Optional icon before the label. */
21
+ startIcon?: React.ReactNode;
22
+ /** Optional icon after the label — the external-link or arrow affordance. */
23
+ endIcon?: React.ReactNode;
24
+ className?: string;
25
+ children?: React.ReactNode;
26
+ /**
27
+ * @deprecated Use `isDisabled`. Accepted as an alias for one minor version.
28
+ */
29
+ disabled?: boolean;
30
+ }
31
+ /**
32
+ * Link — Figma `Link` (774:1516).
33
+ *
34
+ * Renders an `<a>` when given an `href` and a `<button>` otherwise, the same
35
+ * judgment `NavItem` makes: the element follows what the caller is actually
36
+ * building rather than a separate `as` prop. A link that does not navigate is
37
+ * a button, and shipping one as an anchor breaks middle-click, "open in new
38
+ * tab" and the screen-reader announcement all at once.
39
+ *
40
+ * NO SIZE PROP, DELIBERATELY. A link is an inline element and inherits its
41
+ * type from the text around it — `font-size: inherit` in the stylesheet, and
42
+ * icons sized in `em` so they scale with it. Figma has to draw its variants at
43
+ * some concrete size (16/24, `type/body`), but that is one sample of an
44
+ * inheriting element, not a specification. Do not add a size ladder to match
45
+ * the drawing.
46
+ */
47
+ export declare const Link: React.ForwardRefExoticComponent<LinkProps & React.RefAttributes<HTMLAnchorElement & HTMLButtonElement>>;
48
+ export {};
49
+ //# sourceMappingURL=Link.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Link.d.ts","sourceRoot":"","sources":["../../src/components/Link.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAkD,MAAM,OAAO,CAAC;AAQvE,OAAO,KAAK,EAAE,eAAe,EAAqB,MAAM,YAAY,CAAC;AAGrE,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,YAAY,CAAC;AAElD;;;;GAIG;AACH,KAAK,YAAY,GAAG,IAAI,CACtB,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAC7C,MAAM,eAAe,GAAG,OAAO,CAChC,CAAC;AAEF,MAAM,WAAW,SAAU,SAAQ,eAAe,EAAE,YAAY;IAC9D;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,sCAAsC;IACtC,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC5B,6EAA6E;IAC7E,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,IAAI,yGAyHf,CAAC"}
@@ -0,0 +1,77 @@
1
+ 'use client';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, useRef, useImperativeHandle } from 'react';
4
+ import { useLink, useButton, useHover, useFocusRing, mergeProps, } from 'react-aria';
5
+ import { resolveDisabled } from './resolve-disabled.js';
6
+ /**
7
+ * Link — Figma `Link` (774:1516).
8
+ *
9
+ * Renders an `<a>` when given an `href` and a `<button>` otherwise, the same
10
+ * judgment `NavItem` makes: the element follows what the caller is actually
11
+ * building rather than a separate `as` prop. A link that does not navigate is
12
+ * a button, and shipping one as an anchor breaks middle-click, "open in new
13
+ * tab" and the screen-reader announcement all at once.
14
+ *
15
+ * NO SIZE PROP, DELIBERATELY. A link is an inline element and inherits its
16
+ * type from the text around it — `font-size: inherit` in the stylesheet, and
17
+ * icons sized in `em` so they scale with it. Figma has to draw its variants at
18
+ * some concrete size (16/24, `type/body`), but that is one sample of an
19
+ * inheriting element, not a specification. Do not add a size ladder to match
20
+ * the drawing.
21
+ */
22
+ export const Link = forwardRef((props, forwardedRef) => {
23
+ const { variant = 'inline', startIcon, endIcon, className: customClassName, children, isDisabled: isDisabledProp, disabled, href, ...restProps } = props;
24
+ const isDisabled = resolveDisabled(isDisabledProp, disabled);
25
+ const domRef = useRef(null);
26
+ useImperativeHandle(forwardedRef, () => domRef.current);
27
+ const isNavigating = Boolean(href);
28
+ /*
29
+ * `useLink` for the anchor, `useButton` for the button — NOT `useLink` for
30
+ * both.
31
+ *
32
+ * `useLink` on a non-anchor stamps `role="link"` on it, which is the correct
33
+ * behaviour for a hand-rolled link and precisely wrong here: this component
34
+ * renders a real `<button>` when there is nothing to navigate to, and a
35
+ * button announced as a link tells a screen-reader user to expect navigation
36
+ * that will not happen. Caught by a story asserting `getByRole('button')`.
37
+ *
38
+ * Both hooks are called unconditionally because React forbids conditional
39
+ * hooks and `href` can change between renders. Only one result is ever
40
+ * spread, and both deliver their behaviour entirely through the props they
41
+ * return, so the unused one attaches nothing to the DOM.
42
+ */
43
+ const link = useLink({ ...props, isDisabled, elementType: 'a' }, domRef);
44
+ const button = useButton({
45
+ ...props,
46
+ isDisabled,
47
+ elementType: 'button',
48
+ }, domRef);
49
+ const interactionProps = isNavigating ? link.linkProps : button.buttonProps;
50
+ const isPressed = isNavigating ? link.isPressed : button.isPressed;
51
+ const { hoverProps, isHovered } = useHover({ isDisabled });
52
+ const { focusProps, isFocusVisible } = useFocusRing();
53
+ const classNames = ['ion-link', `ion-link--${variant}`, customClassName || '']
54
+ .filter(Boolean)
55
+ .join(' ');
56
+ // `undefined` rather than `false`, so the attribute is omitted entirely
57
+ // instead of rendering data-hovered="false" — which would still match an
58
+ // `[data-hovered]` selector.
59
+ const stateAttributes = {
60
+ 'data-hovered': isHovered || undefined,
61
+ 'data-pressed': isPressed || undefined,
62
+ 'data-focused': isFocusVisible || undefined,
63
+ 'data-disabled': isDisabled || undefined,
64
+ };
65
+ const content = (_jsxs(_Fragment, { children: [startIcon && (_jsx("span", { className: "ion-link__icon-start", "aria-hidden": "true", children: startIcon })), children && _jsx("span", { className: "ion-link__label", children: children }), endIcon && (_jsx("span", { className: "ion-link__icon-end", "aria-hidden": "true", children: endIcon }))] }));
66
+ if (!href) {
67
+ return (_jsx("button", { ...restProps, ...mergeProps(interactionProps, hoverProps, focusProps), ...stateAttributes, ref: domRef, type: "button", disabled: isDisabled, className: classNames, children: content }));
68
+ }
69
+ return (_jsx("a", { ...restProps, ...mergeProps(interactionProps, hoverProps, focusProps), ...stateAttributes, ref: domRef,
70
+ // A disabled link must not remain navigable. `aria-disabled` alone
71
+ // still leaves the href clickable and focusable, so the href is
72
+ // dropped and it is taken out of the tab order — the same treatment
73
+ // NavItem gives a disabled link.
74
+ href: isDisabled ? undefined : href, "aria-disabled": isDisabled || undefined, tabIndex: isDisabled ? -1 : undefined, className: classNames, children: content }));
75
+ });
76
+ Link.displayName = 'Link';
77
+ //# sourceMappingURL=Link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Link.js","sourceRoot":"","sources":["../../src/components/Link.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import React from 'react';
2
+ import type { AriaModalOverlayProps } from 'react-aria';
3
+ export type ModalSize = 'sm' | 'md' | 'lg' | 'fullscreen';
4
+ export type ModalAlign = 'left' | 'center';
5
+ export interface ModalProps extends AriaModalOverlayProps {
6
+ /** Whether the modal is shown. */
7
+ isOpen?: boolean;
8
+ /** Called with the next open state — `false` when the modal asks to close. */
9
+ onOpenChange?: (isOpen: boolean) => void;
10
+ /** Matches the Figma `Size` variant. */
11
+ size?: ModalSize;
12
+ /** Matches the Figma `Align` variant. */
13
+ align?: ModalAlign;
14
+ /** Required: the dialog's accessible name, wired via `aria-labelledby`. */
15
+ title: React.ReactNode;
16
+ /** Supporting copy under the title. */
17
+ description?: React.ReactNode;
18
+ /**
19
+ * The featured slot above the title — an icon or an illustration. Rendered
20
+ * inside a circular container that hugs it.
21
+ */
22
+ media?: React.ReactNode;
23
+ /** Action row. Right-aligned when `align="left"`, centred when `"center"`. */
24
+ footer?: React.ReactNode;
25
+ /** Show the close button. Defaults to `true`. */
26
+ showClose?: boolean;
27
+ /** Accessible name for the close button. */
28
+ closeLabel?: string;
29
+ /** Body content. */
30
+ children?: React.ReactNode;
31
+ className?: string;
32
+ }
33
+ /**
34
+ * Modal — Figma `Modal` (792:1537).
35
+ *
36
+ * `Size` x `Align`, with the sections as props rather than variants. The panel
37
+ * hugs its content at every size except `fullscreen`, which fills the viewport
38
+ * and pushes the actions to the bottom edge.
39
+ *
40
+ * RENDERS NOTHING WHEN CLOSED, and mounts into a portal when open. Both come
41
+ * from React Aria's `Overlay`, and both matter: a modal left in the tree while
42
+ * closed is still focusable by keyboard, and one rendered inline inherits any
43
+ * `overflow: hidden` or stacking context from wherever it was written.
44
+ *
45
+ * The scrim is part of this component in code, unlike in Figma — see the note
46
+ * in modal.css.
47
+ */
48
+ export declare const Modal: React.ForwardRefExoticComponent<ModalProps & React.RefAttributes<HTMLDivElement>>;
49
+ //# sourceMappingURL=Modal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../src/components/Modal.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAkD,MAAM,OAAO,CAAC;AASvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAExD,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,YAAY,CAAC;AAC1D,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAC;AAkB3C,MAAM,WAAW,UAAW,SAAQ,qBAAqB;IACvD,kCAAkC;IAClC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACzC,wCAAwC;IACxC,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,yCAAyC;IACzC,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,2EAA2E;IAC3E,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,uCAAuC;IACvC,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACzB,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oBAAoB;IACpB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0FD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,KAAK,mFAUhB,CAAC"}
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, useRef, useImperativeHandle } from 'react';
4
+ import { Overlay, useModalOverlay, useDialog, useButton, mergeProps, } from 'react-aria';
5
+ import { useOverlayTriggerState } from 'react-stately';
6
+ /** The close glyph from Figma's `Close` slot, inlined for the same reason
7
+ * Select's and PhoneInput's chevrons are: it is part of the component, not a
8
+ * slot a caller fills, and `ionbase-ui` deliberately does not depend on
9
+ * `ionbase-icons`. */
10
+ const CloseIcon = () => (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M18 6 6 18M6 6l12 12", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
11
+ function ModalDialog(props, forwardedRef) {
12
+ const { state, size = 'md', align = 'left', title, description, media, footer, showClose = true, closeLabel = 'Close dialog', children, className, } = props;
13
+ const panelRef = useRef(null);
14
+ useImperativeHandle(forwardedRef, () => panelRef.current);
15
+ /*
16
+ * `useModalOverlay` supplies what makes this a modal rather than a styled
17
+ * box: it traps focus inside the panel, marks everything outside it
18
+ * `aria-hidden`, prevents the page behind from scrolling, and closes on
19
+ * Escape and on an outside click. `useDialog` adds the dialog role and
20
+ * moves focus in on open.
21
+ */
22
+ const { modalProps, underlayProps } = useModalOverlay(props, state, panelRef);
23
+ const { dialogProps, titleProps } = useDialog({ role: 'dialog' }, panelRef);
24
+ const closeRef = useRef(null);
25
+ const { buttonProps: closeButtonProps } = useButton({ onPress: () => state.close(), 'aria-label': closeLabel }, closeRef);
26
+ const panelClassNames = [
27
+ 'ion-modal',
28
+ `ion-modal--${size}`,
29
+ `ion-modal--${align}`,
30
+ className || '',
31
+ ]
32
+ .filter(Boolean)
33
+ .join(' ');
34
+ return (_jsx("div", { ...underlayProps, className: "ion-modal__scrim", children: _jsxs("div", { ...mergeProps(modalProps, dialogProps), ref: panelRef, className: panelClassNames, children: [_jsxs("div", { className: "ion-modal__header", children: [_jsxs("div", { className: "ion-modal__heading", children: [media && (_jsx("div", { className: "ion-modal__media", "aria-hidden": "true", children: media })), _jsx("h2", { ...titleProps, className: "ion-modal__title", children: title }), description && (_jsx("p", { className: "ion-modal__description", children: description }))] }), showClose && (_jsx("button", { ...closeButtonProps, ref: closeRef, type: "button", className: "ion-modal__close", children: _jsx(CloseIcon, {}) }))] }), children && _jsx("div", { className: "ion-modal__body", children: children }), footer && _jsx("div", { className: "ion-modal__footer", children: footer })] }) }));
35
+ }
36
+ const ModalDialogWithRef = forwardRef(ModalDialog);
37
+ /**
38
+ * Modal — Figma `Modal` (792:1537).
39
+ *
40
+ * `Size` x `Align`, with the sections as props rather than variants. The panel
41
+ * hugs its content at every size except `fullscreen`, which fills the viewport
42
+ * and pushes the actions to the bottom edge.
43
+ *
44
+ * RENDERS NOTHING WHEN CLOSED, and mounts into a portal when open. Both come
45
+ * from React Aria's `Overlay`, and both matter: a modal left in the tree while
46
+ * closed is still focusable by keyboard, and one rendered inline inherits any
47
+ * `overflow: hidden` or stacking context from wherever it was written.
48
+ *
49
+ * The scrim is part of this component in code, unlike in Figma — see the note
50
+ * in modal.css.
51
+ */
52
+ export const Modal = forwardRef((props, ref) => {
53
+ const state = useOverlayTriggerState(props);
54
+ if (!state.isOpen)
55
+ return null;
56
+ return (_jsx(Overlay, { children: _jsx(ModalDialogWithRef, { ...props, state: state, ref: ref }) }));
57
+ });
58
+ Modal.displayName = 'Modal';
59
+ //# sourceMappingURL=Modal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Modal.js","sourceRoot":"","sources":["../../src/components/Modal.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+ import { type InputProps } from './Input.js';
3
+ export interface PhoneInputProps extends Omit<InputProps, 'leadingAddon' | 'leadingIcon'> {
4
+ /** The dial code shown in the leading block, e.g. `+1`. */
5
+ dialCode?: string;
6
+ /**
7
+ * Accessible name for the dial-code trigger. It shows only `+1`, which names
8
+ * a value rather than an action, so the button needs a label of its own.
9
+ */
10
+ countryLabel?: string;
11
+ /**
12
+ * Spread onto the dial-code `<button>`. This is how a country picker gets
13
+ * attached — see the note on scope below.
14
+ */
15
+ countryButtonProps?: React.ButtonHTMLAttributes<HTMLButtonElement>;
16
+ }
17
+ /**
18
+ * PhoneInput — Figma `Input/Phone` (80:372).
19
+ *
20
+ * A dial-code block butted against an Input, sharing one outline: the block
21
+ * carries the left radii and the control the right, so the seam is square on
22
+ * both sides and reads as a single control. Both keep their full 1px border,
23
+ * which is what Figma draws — the 2px seam is the two strokes meeting, not an
24
+ * accident.
25
+ *
26
+ * Three sizes, no State axis. Every interaction state is the Input's, reached
27
+ * through the ordinary props, because Figma composes this from the same `Input`
28
+ * instance rather than redrawing it.
29
+ *
30
+ * WHAT THIS DELIBERATELY DOES NOT DO
31
+ *
32
+ * It does not pick countries. The chevron implies a menu and Figma specifies no
33
+ * open state for it — no list, no flags, no search, no selected state. Country
34
+ * data and the picker are application concerns with real editorial weight (which
35
+ * territories, which names, which order), and inventing them here would be
36
+ * designing rather than implementing.
37
+ *
38
+ * So the trigger is a real `<button>` with an accessible name, and
39
+ * `countryButtonProps` is how you wire it to a `Menu`, a popover or your own
40
+ * listbox — including the `aria-haspopup` and `aria-expanded` that only the
41
+ * thing owning the popup can set honestly.
42
+ */
43
+ export declare const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps & React.RefAttributes<HTMLInputElement>>;
44
+ //# sourceMappingURL=PhoneInput.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PhoneInput.d.ts","sourceRoot":"","sources":["../../src/components/PhoneInput.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAqB,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAS,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAwBpD,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAC3C,UAAU,EACV,cAAc,GAAG,aAAa,CAC/B;IACC,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,CAAC;CACpE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,UAAU,0FAqDtB,CAAC"}
@@ -0,0 +1,67 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /*
3
+ * No `'use client'` here, deliberately — the build asserts its absence.
4
+ *
5
+ * This component holds no state and calls no hook: it composes a button and
6
+ * `Input`, and `Input` carries the boundary already. Marking it would pull an
7
+ * otherwise server-renderable module out of the server graph for nothing.
8
+ */
9
+ import { forwardRef } from 'react';
10
+ import { Input } from './Input.js';
11
+ import { resolveDisabled } from './resolve-disabled.js';
12
+ /**
13
+ * The chevron from Figma's `Chevron` slot, inlined.
14
+ *
15
+ * Same reasoning as Select's: the shape never varies and is part of the
16
+ * component rather than a slot a consumer fills, so drawing it here avoids
17
+ * making `ionbase-ui` depend on `ionbase-icons` — a dependency the package
18
+ * deliberately does not have. Sizing comes from the CSS, so it tracks the size
19
+ * modifier without a prop.
20
+ */
21
+ const ChevronDown = () => (_jsx("svg", { viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "m6 9 6 6 6-6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
22
+ /**
23
+ * PhoneInput — Figma `Input/Phone` (80:372).
24
+ *
25
+ * A dial-code block butted against an Input, sharing one outline: the block
26
+ * carries the left radii and the control the right, so the seam is square on
27
+ * both sides and reads as a single control. Both keep their full 1px border,
28
+ * which is what Figma draws — the 2px seam is the two strokes meeting, not an
29
+ * accident.
30
+ *
31
+ * Three sizes, no State axis. Every interaction state is the Input's, reached
32
+ * through the ordinary props, because Figma composes this from the same `Input`
33
+ * instance rather than redrawing it.
34
+ *
35
+ * WHAT THIS DELIBERATELY DOES NOT DO
36
+ *
37
+ * It does not pick countries. The chevron implies a menu and Figma specifies no
38
+ * open state for it — no list, no flags, no search, no selected state. Country
39
+ * data and the picker are application concerns with real editorial weight (which
40
+ * territories, which names, which order), and inventing them here would be
41
+ * designing rather than implementing.
42
+ *
43
+ * So the trigger is a real `<button>` with an accessible name, and
44
+ * `countryButtonProps` is how you wire it to a `Menu`, a popover or your own
45
+ * listbox — including the `aria-haspopup` and `aria-expanded` that only the
46
+ * thing owning the popup can set honestly.
47
+ */
48
+ export const PhoneInput = forwardRef((props, forwardedRef) => {
49
+ const { dialCode = '+1', countryLabel = 'Select country calling code', countryButtonProps, size = 'md', type = 'tel', isDisabled: isDisabledProp, disabled, ...inputProps } = props;
50
+ const isDisabled = resolveDisabled(isDisabledProp, disabled);
51
+ const countryClassNames = [
52
+ 'ion-phone-input__country',
53
+ size !== 'md' ? `ion-phone-input__country--${size}` : '',
54
+ countryButtonProps?.className || '',
55
+ ]
56
+ .filter(Boolean)
57
+ .join(' ');
58
+ const country = (_jsxs("button", { type: "button", "aria-label": countryLabel, ...countryButtonProps, className: countryClassNames,
59
+ // Read from the resolved value rather than the raw prop, so the
60
+ // deprecated `disabled` alias disables the trigger too. A dial-code
61
+ // button that stays live beside a disabled field is a real trap: it
62
+ // looks operable and would open a picker for a field you cannot edit.
63
+ disabled: isDisabled || countryButtonProps?.disabled, children: [_jsx("span", { className: "ion-phone-input__dial-code", children: dialCode }), _jsx("span", { className: "ion-phone-input__chevron", children: _jsx(ChevronDown, {}) })] }));
64
+ return (_jsx(Input, { ...inputProps, ref: forwardedRef, size: size, type: type, isDisabled: isDisabled, leadingAddon: country }));
65
+ });
66
+ PhoneInput.displayName = 'PhoneInput';
67
+ //# sourceMappingURL=PhoneInput.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PhoneInput.js","sourceRoot":"","sources":["../../src/components/PhoneInput.tsx"],"names":[],"mappings":";AAAA;;;;;;GAMG;AACH,OAAc,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAmB,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD;;;;;;;;GAQG;AACH,MAAM,WAAW,GAAG,GAAG,EAAE,CAAC,CACxB,cAAK,OAAO,EAAC,WAAW,EAAC,IAAI,EAAC,MAAM,iBAAa,MAAM,EAAC,SAAS,EAAC,OAAO,YACvE,eACE,CAAC,EAAC,cAAc,EAChB,MAAM,EAAC,cAAc,EACrB,WAAW,EAAC,GAAG,EACf,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,GACtB,GACE,CACP,CAAC;AAoBF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,UAAU,CAClC,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE;IACtB,MAAM,EACJ,QAAQ,GAAG,IAAI,EACf,YAAY,GAAG,6BAA6B,EAC5C,kBAAkB,EAClB,IAAI,GAAG,IAAI,EACX,IAAI,GAAG,KAAK,EACZ,UAAU,EAAE,cAAc,EAC1B,QAAQ,EACR,GAAG,UAAU,EACd,GAAG,KAAK,CAAC;IAEV,MAAM,UAAU,GAAG,eAAe,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;IAE7D,MAAM,iBAAiB,GAAG;QACxB,0BAA0B;QAC1B,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,6BAA6B,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE;QACxD,kBAAkB,EAAE,SAAS,IAAI,EAAE;KACpC;SACE,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC,CAAC;IAEb,MAAM,OAAO,GAAG,CACd,kBACE,IAAI,EAAC,QAAQ,gBACD,YAAY,KACpB,kBAAkB,EACtB,SAAS,EAAE,iBAAiB;QAC5B,gEAAgE;QAChE,oEAAoE;QACpE,oEAAoE;QACpE,sEAAsE;QACtE,QAAQ,EAAE,UAAU,IAAI,kBAAkB,EAAE,QAAQ,aAEpD,eAAM,SAAS,EAAC,4BAA4B,YAAE,QAAQ,GAAQ,EAC9D,eAAM,SAAS,EAAC,0BAA0B,YACxC,KAAC,WAAW,KAAG,GACV,IACA,CACV,CAAC;IAEF,OAAO,CACL,KAAC,KAAK,OACA,UAAU,EACd,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,UAAU,EACtB,YAAY,EAAE,OAAO,GACrB,CACH,CAAC;AACJ,CAAC,CACF,CAAC;AAEF,UAAU,CAAC,WAAW,GAAG,YAAY,CAAC"}
@@ -6,6 +6,8 @@ export { Badge } from './Badge.js';
6
6
  export type { BadgeProps, BadgeIntent } from './Badge.js';
7
7
  export { Input } from './Input.js';
8
8
  export type { InputProps, InputSize } from './Input.js';
9
+ export { PhoneInput } from './PhoneInput.js';
10
+ export type { PhoneInputProps } from './PhoneInput.js';
9
11
  export { Select } from './Select.js';
10
12
  export type { SelectProps, SelectSize, SelectOption } from './Select.js';
11
13
  export { Checkbox } from './Checkbox.js';
@@ -24,6 +26,10 @@ export { Logo, LogoMark } from './Logo.js';
24
26
  export type { LogoProps, LogoMarkProps, LogoSize, LogoWordmark, } from './Logo.js';
25
27
  export { Divider } from './Divider.js';
26
28
  export type { DividerProps, DividerOrientation } from './Divider.js';
29
+ export { Modal } from './Modal.js';
30
+ export type { ModalProps, ModalSize, ModalAlign } from './Modal.js';
31
+ export { Link } from './Link.js';
32
+ export type { LinkProps, LinkVariant } from './Link.js';
27
33
  export { NavItem } from './NavItem.js';
28
34
  export type { NavItemProps } from './NavItem.js';
29
35
  export { Table, TableHead, TableBody, TableRow, TableCell } from './Table.js';
@@ -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;AAC7B,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,7 @@ export { Button } from './Button.js';
2
2
  export { Tabs, TabItem } from './Tabs.js';
3
3
  export { Badge } from './Badge.js';
4
4
  export { Input } from './Input.js';
5
+ export { PhoneInput } from './PhoneInput.js';
5
6
  export { Select } from './Select.js';
6
7
  export { Checkbox } from './Checkbox.js';
7
8
  export { Radio, RadioGroup } from './Radio.js';
@@ -11,6 +12,8 @@ export { Avatar, AvatarGroup } from './Avatar.js';
11
12
  export { Header } from './Header.js';
12
13
  export { Logo, LogoMark } from './Logo.js';
13
14
  export { Divider } from './Divider.js';
15
+ export { Modal } from './Modal.js';
16
+ export { Link } from './Link.js';
14
17
  export { NavItem } from './NavItem.js';
15
18
  export { Table, TableHead, TableBody, TableRow, TableCell } from './Table.js';
16
19
  export { ScrollProgress } from './ScrollProgress.js';
@@ -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;AAKrD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,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,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,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,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,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"}
@@ -31,6 +31,7 @@
31
31
  @import url('./tabs.css');
32
32
  @import url('./badge.css');
33
33
  @import url('./input.css');
34
+ @import url('./phone-input.css');
34
35
  @import url('./select.css');
35
36
  @import url('./checkbox.css');
36
37
  @import url('./radio.css');
@@ -41,6 +42,8 @@
41
42
  @import url('./logo.css');
42
43
  @import url('./table.css');
43
44
  @import url('./divider.css');
45
+ @import url('./link.css');
46
+ @import url('./modal.css');
44
47
  @import url('./nav-item.css');
45
48
  @import url('./scroll-progress.css');
46
49
  @import url('./full-card.css');
@@ -89,6 +89,25 @@
89
89
  --ion-input-padding-x: var(--spacing-16);
90
90
  }
91
91
 
92
+ /*
93
+ * A leading addon and the control, sharing one outline — Figma `Input/Phone`
94
+ * (80:372).
95
+ *
96
+ * `align-items: stretch` rather than a height here on purpose: the addon takes
97
+ * its height from the control, so the size ladder stays in exactly one place
98
+ * and the two halves cannot drift. The addon supplies its own left radii; this
99
+ * rule only has to square off the edge they meet on.
100
+ */
101
+ .ion-input-group {
102
+ display: flex;
103
+ align-items: stretch;
104
+ }
105
+
106
+ .ion-input-group > .ion-input {
107
+ border-start-start-radius: 0;
108
+ border-end-start-radius: 0;
109
+ }
110
+
92
111
  /*
93
112
  * The field itself carries no box of its own — the wrapper owns the border,
94
113
  * background and padding so the icons sit inside it. Everything here is either
@@ -0,0 +1,181 @@
1
+ /*
2
+ * Link
3
+ *
4
+ * Measured from Figma `Link` (774:1516). Two types x five states.
5
+ *
6
+ * NO SIZE LADDER, AND THAT IS THE POINT
7
+ *
8
+ * Every other control here pins its own type ramp because it owns a box. A
9
+ * link owns nothing: it sits inside a sentence and has to be the size of that
10
+ * sentence. So `font-size` and `line-height` inherit, and the icons are sized
11
+ * in `em` rather than from the `icon-size` ladder — a 14px link gets a 14px
12
+ * icon without being told.
13
+ *
14
+ * Figma draws its variants at 16/24 (`type/body`) because a Figma variant must
15
+ * have a concrete size. That is one sample of an inheriting element, not a
16
+ * specification, and the drawing should not be used to justify adding a size
17
+ * prop here.
18
+ */
19
+ .ion-link {
20
+ display: inline-flex;
21
+ align-items: center;
22
+ gap: var(--spacing-4);
23
+
24
+ /*
25
+ * `baseline` rather than the flex default, so a link with an icon still sits
26
+ * on the same baseline as the prose around it. `inline-flex` alone would
27
+ * align the whole box to the text baseline and push the label off it.
28
+ */
29
+ vertical-align: baseline;
30
+ color: var(--text-link);
31
+ font-family: inherit;
32
+ font-size: inherit;
33
+ line-height: inherit;
34
+ font-weight: inherit;
35
+ cursor: pointer;
36
+
37
+ /* Reset for the <button> form — it is a link, not a control with a box. */
38
+ padding: 0;
39
+ background: none;
40
+ border: none;
41
+ transition: color var(--ion-duration-base) var(--ion-ease-out);
42
+ }
43
+
44
+ /*
45
+ * Inline is underlined in EVERY state.
46
+ *
47
+ * This is the accessibility half of the Type axis, not a stylistic preference:
48
+ * a link inside a paragraph that is distinguished only by colour fails WCAG
49
+ * 1.4.1. `text-underline-offset` keeps the rule off the descenders, which is
50
+ * what makes an always-on underline tolerable in body copy.
51
+ */
52
+ .ion-link--inline {
53
+ text-decoration: underline;
54
+ text-underline-offset: 0.2em;
55
+ }
56
+
57
+ /*
58
+ * Standalone earns its lack of underline from context — a table cell, a list,
59
+ * a card footer — where position already says "interactive". It still
60
+ * underlines on hover, so the affordance is never absent, only deferred.
61
+ */
62
+ .ion-link--standalone {
63
+ text-decoration: none;
64
+ }
65
+
66
+ /*
67
+ * Hover is declared twice, as it is on Button and Input. Native `:hover`
68
+ * latches on touch until you tap elsewhere; `[data-hovered]` comes from React
69
+ * Aria's useHover, which is pointer-aware. The media query keeps the
70
+ * stylesheet working without React.
71
+ */
72
+ @media (hover: hover) {
73
+ .ion-link:hover {
74
+ color: var(--text-link-hover);
75
+ }
76
+
77
+ .ion-link--standalone:hover {
78
+ text-decoration: underline;
79
+ text-underline-offset: 0.2em;
80
+ }
81
+ }
82
+
83
+ .ion-link[data-hovered='true'] {
84
+ color: var(--text-link-hover);
85
+ }
86
+
87
+ .ion-link--standalone[data-hovered='true'] {
88
+ text-decoration: underline;
89
+ text-underline-offset: 0.2em;
90
+ }
91
+
92
+ /*
93
+ * Visited is colour-only, and it has to be.
94
+ *
95
+ * Browsers deliberately restrict `:visited` to a short list of properties to
96
+ * close a history-sniffing side channel — `getComputedStyle` lies about it,
97
+ * and layout-affecting properties are ignored outright. Colour is on the
98
+ * allowed list, which is exactly why `text/link/visited` is a colour and why
99
+ * this rule tries nothing else.
100
+ *
101
+ * Only anchors have history, so this is scoped to `a`. The `<button>` form has
102
+ * no visited state to express.
103
+ */
104
+ a.ion-link:visited {
105
+ color: var(--text-link-visited);
106
+ }
107
+
108
+ /* Hover after visited, so a visited link still responds to the pointer. */
109
+ @media (hover: hover) {
110
+ a.ion-link:visited:hover {
111
+ color: var(--text-link-hover);
112
+ }
113
+ }
114
+
115
+ a.ion-link:visited[data-hovered='true'] {
116
+ color: var(--text-link-hover);
117
+ }
118
+
119
+ /*
120
+ * Focus is a ring, not the `Focus/md` effect style every boxed control uses.
121
+ *
122
+ * That style is a drop shadow, which needs a filled box to cast from — a link
123
+ * has no surface, so it rendered as a faint glow off the glyph shapes instead
124
+ * of a ring. `ring/focus` is the token for this and had no consumer anywhere
125
+ * until this component.
126
+ *
127
+ * `outline-offset` is what Figma cannot express: there, the ring hugs the text
128
+ * because adding padding would change the box and knock an inline link out of
129
+ * alignment with its sentence. An outline is drawn outside the box and costs
130
+ * no layout, so the breathing room is free here.
131
+ */
132
+ .ion-link:focus-visible,
133
+ .ion-link[data-focused='true'] {
134
+ outline: var(--border-width-thick) solid var(--ring-focus);
135
+ outline-offset: 2px;
136
+ border-radius: var(--radius-xs);
137
+ }
138
+
139
+ .ion-link--disabled,
140
+ .ion-link:disabled,
141
+ .ion-link[data-disabled='true'] {
142
+ color: var(--text-disabled);
143
+ cursor: not-allowed;
144
+ }
145
+
146
+ /*
147
+ * Icons take the label's colour rather than an `icon/*` role.
148
+ *
149
+ * There is deliberately no `color` declaration here, and its absence is the
150
+ * mechanism rather than an omission. Colour inherits, so the icon picks up
151
+ * every state — hover, visited, disabled — without a rule of its own and
152
+ * cannot drift from the text it belongs to. A `color: currentColor` line would
153
+ * restate the default while reading as though something were being decided.
154
+ *
155
+ * This is deliberately not the usual v2 split between `text/*` and `icon/*`:
156
+ * the icon makes no colour decision at all. Figma binds the label's token onto
157
+ * the icon vectors to depict the same relationship, because Figma has no
158
+ * inheritance — which is why the icons sat at `icon/default` (near-black) on a
159
+ * blue link until it was caught in review.
160
+ */
161
+ .ion-link__icon-start,
162
+ .ion-link__icon-end {
163
+ display: inline-flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ flex-shrink: 0;
167
+
168
+ /* The underline belongs to the text, never to the icons. */
169
+ text-decoration: none;
170
+ }
171
+
172
+ /*
173
+ * `em`, not the `icon-size` ladder — the icon scales with whatever type the
174
+ * link inherited. `1em` against a 16px label is 16px, which is `icon-size/sm`,
175
+ * the rung Figma draws.
176
+ */
177
+ .ion-link__icon-start svg,
178
+ .ion-link__icon-end svg {
179
+ width: 1em;
180
+ height: 1em;
181
+ }
@@ -0,0 +1,235 @@
1
+ /*
2
+ * Modal
3
+ *
4
+ * Measured from Figma `Modal` (792:1537). Four sizes x two alignments.
5
+ *
6
+ * THE SCRIM IS IN THIS COMPONENT, AND IN FIGMA IT IS NOT
7
+ *
8
+ * Figma models the panel alone so it can be dropped into any layout, and binds
9
+ * `surface/scrim` on a separate `Overlay — usage` frame instead. In code that
10
+ * split would be useless: nothing can render a modal without also rendering
11
+ * what sits behind it, so the scrim lives here.
12
+ *
13
+ * The consequence is that `bindings.json` records the panel's tokens but not
14
+ * the scrim, because the export only walks components and the scrim is on a
15
+ * frame. `surface/scrim` therefore still reads as unbound in any "which roles
16
+ * have no consumer" sweep. It has one — this file.
17
+ */
18
+ .ion-modal__scrim {
19
+ position: fixed;
20
+ inset: 0;
21
+ z-index: 1000;
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ padding: var(--spacing-24);
26
+ background-color: var(--surface-scrim);
27
+ }
28
+
29
+ .ion-modal {
30
+ /*
31
+ * Size is a set of slots defaulting to Medium, the same shape Button and
32
+ * Input use — a size modifier reassigns the width rather than restating the
33
+ * whole box.
34
+ */
35
+ --ion-modal-width: 560px;
36
+
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: var(--spacing-20);
40
+ width: var(--ion-modal-width);
41
+
42
+ /*
43
+ * Never taller than the viewport it sits in. Figma's panels hug their
44
+ * content because a Figma frame has no viewport to overflow; a real one
45
+ * does, and a dialog taller than the screen becomes unreachable at both
46
+ * ends. The scrim's padding is subtracted so the panel never touches the
47
+ * edges.
48
+ */
49
+ max-width: 100%;
50
+ max-height: 100%;
51
+ padding: var(--spacing-24);
52
+ background-color: var(--surface-overlay);
53
+ border: var(--border-width-default) solid var(--border-subtle);
54
+ border-radius: var(--radius-xl);
55
+ box-shadow: var(--ion-shadow-shadow-xl);
56
+ }
57
+
58
+ .ion-modal--sm {
59
+ --ion-modal-width: 400px;
60
+ }
61
+
62
+ .ion-modal--lg {
63
+ --ion-modal-width: 720px;
64
+ }
65
+
66
+ /*
67
+ * Fullscreen is a different layout, not another width.
68
+ *
69
+ * It meets the viewport edges, so radius, border and shadow are removed rather
70
+ * than reduced — there is nothing for them to sit against. The scrim's padding
71
+ * goes too, or the "fullscreen" panel would float in a 24px frame.
72
+ */
73
+ .ion-modal--fullscreen {
74
+ --ion-modal-width: 100%;
75
+
76
+ height: 100%;
77
+ border: none;
78
+ border-radius: 0;
79
+ box-shadow: none;
80
+ }
81
+
82
+ .ion-modal__scrim:has(.ion-modal--fullscreen) {
83
+ padding: 0;
84
+ }
85
+
86
+ .ion-modal__header {
87
+ display: flex;
88
+ align-items: flex-start;
89
+ gap: var(--spacing-16);
90
+ }
91
+
92
+ .ion-modal__heading {
93
+ display: flex;
94
+ flex: 1 1 auto;
95
+ min-width: 0;
96
+ flex-direction: column;
97
+ gap: var(--spacing-8);
98
+ }
99
+
100
+ .ion-modal__title {
101
+ margin: 0;
102
+ color: var(--text-default);
103
+ font-family: var(--font-family-sans);
104
+ font-size: var(--type-h5);
105
+ line-height: var(--type-h5-line-height);
106
+ font-weight: var(--font-weight-semibold);
107
+ }
108
+
109
+ .ion-modal__description {
110
+ margin: 0;
111
+ color: var(--text-secondary);
112
+ font-family: var(--font-family-sans);
113
+ font-size: var(--type-body);
114
+ line-height: var(--type-body-line-height);
115
+ font-weight: var(--font-weight-regular);
116
+ }
117
+
118
+ /*
119
+ * The featured slot. Circular, hugging whatever is put in it, so an icon and a
120
+ * spot illustration both sit correctly without a second variant.
121
+ */
122
+ .ion-modal__media {
123
+ display: inline-flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ flex-shrink: 0;
127
+ width: var(--spacing-48);
128
+ height: var(--spacing-48);
129
+ background-color: var(--surface-primary-subtle);
130
+ border-radius: var(--radius-full);
131
+ color: var(--icon-primary);
132
+ }
133
+
134
+ .ion-modal__media svg {
135
+ width: var(--icon-size-lg);
136
+ height: var(--icon-size-lg);
137
+ }
138
+
139
+ /*
140
+ * 32px, not the 20px glyph it contains. WCAG 2.2 SC 2.5.8 asks for a 24x24
141
+ * minimum pointer target and the icon alone would fail it. Do not shrink this
142
+ * to fit the icon.
143
+ */
144
+ .ion-modal__close {
145
+ display: inline-flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ flex-shrink: 0;
149
+ width: var(--spacing-32);
150
+ height: var(--spacing-32);
151
+ padding: 0;
152
+ background: none;
153
+ border: none;
154
+ border-radius: var(--radius-sm);
155
+ color: var(--icon-secondary);
156
+ cursor: pointer;
157
+ transition: background-color var(--ion-duration-base) var(--ion-ease-out);
158
+ }
159
+
160
+ .ion-modal__close svg {
161
+ width: var(--icon-size-md);
162
+ height: var(--icon-size-md);
163
+ }
164
+
165
+ @media (hover: hover) {
166
+ .ion-modal__close:hover {
167
+ background-color: var(--surface-hover);
168
+ }
169
+ }
170
+
171
+ .ion-modal__close:focus-visible {
172
+ outline: var(--border-width-thick) solid var(--ring-focus);
173
+ outline-offset: 2px;
174
+ }
175
+
176
+ /*
177
+ * The body is the only part that scrolls. Capping the panel and letting this
178
+ * flex-shrink keeps the header and actions in place while long content moves
179
+ * underneath them — which is the whole reason a dialog has a header at all.
180
+ */
181
+ .ion-modal__body {
182
+ flex: 1 1 auto;
183
+ min-height: 0;
184
+ overflow-y: auto;
185
+ color: var(--text-secondary);
186
+ font-family: var(--font-family-sans);
187
+ font-size: var(--type-body);
188
+ line-height: var(--type-body-line-height);
189
+ }
190
+
191
+ .ion-modal__footer {
192
+ display: flex;
193
+ align-items: center;
194
+ justify-content: flex-end;
195
+ gap: var(--spacing-12);
196
+ flex-shrink: 0;
197
+ }
198
+
199
+ /*
200
+ * Centred alignment.
201
+ *
202
+ * THE SPACER IS REQUIRED HERE TOO, and assuming otherwise was wrong.
203
+ *
204
+ * The heading is a flex item beside the close button, so it does not span the
205
+ * panel — centring inside it lands the title and media 24px left of the
206
+ * panel's true centre, which is half the close button plus its gap. Exactly
207
+ * the same 24px Figma showed, for exactly the same reason, and the story
208
+ * `CentredContentIsActuallyCentred` measured it.
209
+ *
210
+ * A `::before` rather than a real element, and gated on `:has()` so it exists
211
+ * only when the close button does — mirroring Figma, where the spacer is bound
212
+ * to the same `Show Close` property. A spacer that outlived the button would
213
+ * push the content 24px the other way: the same bug mirrored.
214
+ *
215
+ * The actions centre too. Right-aligned buttons under centred copy read as
216
+ * broken.
217
+ */
218
+ .ion-modal--center .ion-modal__header:has(.ion-modal__close)::before {
219
+ content: '';
220
+ flex-shrink: 0;
221
+ width: var(--spacing-32);
222
+ }
223
+
224
+ .ion-modal--center .ion-modal__heading {
225
+ align-items: center;
226
+ text-align: center;
227
+ }
228
+
229
+ .ion-modal--center .ion-modal__body {
230
+ text-align: center;
231
+ }
232
+
233
+ .ion-modal--center .ion-modal__footer {
234
+ justify-content: center;
235
+ }
@@ -0,0 +1,131 @@
1
+ /*
2
+ * Phone Input
3
+ *
4
+ * Measured from Figma `Input/Phone` (80:372). Three sizes, no State axis —
5
+ * every state belongs to the Input it wraps.
6
+ *
7
+ * THE SEAM IS TWO BORDERS, NOT ONE
8
+ *
9
+ * Figma butts the two frames at x = 68 (Medium) with no gap, and both keep a
10
+ * full 1px stroke on all four sides. So the divider is 2px of `border/default`,
11
+ * and reproducing it needs no negative margin and no shared-edge trick — plain
12
+ * adjacency gives exactly the measured result. The corners facing the seam are
13
+ * square on both sides, which is what makes the pair read as one control:
14
+ * `Country Code` is [r,0,0,r] and the Input is [0,r,r,0], both bound.
15
+ *
16
+ * Radius tracks the size the same way the Input's does — `radius/sm` at Small,
17
+ * `radius/md` at Medium and Large — so the two halves round in step. Getting
18
+ * this wrong is invisible until Small, where an 8px block meets a 6px field.
19
+ */
20
+ .ion-phone-input__country {
21
+ /*
22
+ * Slots, defaulting to Medium, same shape as `.ion-input`. Padding, icon and
23
+ * type deliberately match the Input's rungs at every size, because Figma
24
+ * composes this from that component and a divergence here would show as the
25
+ * dial code sitting off the value's baseline.
26
+ *
27
+ * GAP IS THE ONE THAT DOES NOT MATCH: 4 at every size, where the Input runs
28
+ * 6/8/8. Measured, not inferred — the block holds two tightly-related glyphs
29
+ * rather than an icon and a sentence.
30
+ */
31
+ --ion-phone-input-padding-x: var(--spacing-12);
32
+ --ion-phone-input-gap: var(--spacing-4);
33
+ --ion-phone-input-icon-size: var(--icon-size-md);
34
+ --ion-phone-input-radius: var(--radius-md);
35
+ --ion-phone-input-font-size: var(--type-body);
36
+ --ion-phone-input-line-height: var(--type-body-line-height);
37
+ --ion-phone-input-border-width: var(--border-width-default);
38
+
39
+ display: inline-flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ gap: var(--ion-phone-input-gap);
43
+ flex-shrink: 0;
44
+
45
+ /*
46
+ * No height of its own: the row stretches it to the control beside it, so the
47
+ * two halves cannot drift apart. Pinning a height here would duplicate the
48
+ * Input's size ladder and then disagree with it the first time one moved.
49
+ */
50
+ padding-block: 0;
51
+
52
+ /* Figma's stroke is inside the frame; a CSS border is outside the padding
53
+ * box. Subtracting it puts the dial code where the design has it — the same
54
+ * correction `.ion-input` documents at length. */
55
+ padding-inline: calc(
56
+ var(--ion-phone-input-padding-x) - var(--ion-phone-input-border-width)
57
+ );
58
+ background-color: var(--surface-page);
59
+ border-style: solid;
60
+ border-width: var(--ion-phone-input-border-width);
61
+ border-color: var(--border-default);
62
+ border-start-start-radius: var(--ion-phone-input-radius);
63
+ border-end-start-radius: var(--ion-phone-input-radius);
64
+ border-start-end-radius: 0;
65
+ border-end-end-radius: 0;
66
+ color: var(--text-secondary);
67
+ font-family: var(--font-family-sans);
68
+ font-size: var(--ion-phone-input-font-size);
69
+ line-height: var(--ion-phone-input-line-height);
70
+ font-weight: var(--font-weight-regular);
71
+ cursor: pointer;
72
+ transition:
73
+ border-color var(--ion-duration-base) var(--ion-ease-out),
74
+ background-color var(--ion-duration-base) var(--ion-ease-out);
75
+ }
76
+
77
+ .ion-phone-input__country--sm {
78
+ --ion-phone-input-icon-size: var(--icon-size-sm);
79
+ --ion-phone-input-radius: var(--radius-sm);
80
+ --ion-phone-input-font-size: var(--type-body-sm);
81
+ --ion-phone-input-line-height: var(--type-body-sm-line-height);
82
+ }
83
+
84
+ /* Large grows its padding only — icon, radius and type stay at Medium's rung,
85
+ * exactly as `.ion-input--lg` does. */
86
+ .ion-phone-input__country--lg {
87
+ --ion-phone-input-padding-x: var(--spacing-16);
88
+ }
89
+
90
+ .ion-phone-input__chevron {
91
+ display: inline-flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ flex-shrink: 0;
95
+ color: var(--icon-tertiary);
96
+ }
97
+
98
+ .ion-phone-input__chevron svg {
99
+ width: var(--ion-phone-input-icon-size);
100
+ height: var(--ion-phone-input-icon-size);
101
+ }
102
+
103
+ @media (hover: hover) {
104
+ .ion-phone-input__country:hover:not(:disabled) {
105
+ border-color: var(--border-strong);
106
+ }
107
+ }
108
+
109
+ /*
110
+ * Raised above the control while focused so the ring is not clipped by the
111
+ * neighbour's border. Inset offset because Figma draws focus as an inside
112
+ * stroke — matching `.ion-input`, which thickens its border rather than
113
+ * painting an outline outside the box.
114
+ */
115
+ .ion-phone-input__country:focus-visible {
116
+ outline: var(--border-width-thick) solid var(--border-focus);
117
+ outline-offset: calc(-1 * var(--border-width-thick));
118
+ position: relative;
119
+ z-index: 1;
120
+ }
121
+
122
+ .ion-phone-input__country:disabled {
123
+ background-color: var(--surface-disabled);
124
+ border-color: var(--border-disabled);
125
+ color: var(--text-disabled);
126
+ cursor: not-allowed;
127
+ }
128
+
129
+ .ion-phone-input__country:disabled .ion-phone-input__chevron {
130
+ color: var(--icon-disabled);
131
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ionbase-ui",
3
- "version": "0.8.0",
3
+ "version": "0.11.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",