@moodlehq/design-system 4.0.0 → 5.0.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.
Files changed (81) hide show
  1. package/README.md +16 -6
  2. package/dist/components/_index.legacy.scss +1893 -0
  3. package/dist/components/activity-icon/ActivityIcon.d.ts +3 -3
  4. package/dist/components/activity-icon/ActivityIcon.js +5 -5
  5. package/dist/components/activity-icon/ActivityIcon.js.map +1 -1
  6. package/dist/components/activity-icon/index.css +99 -0
  7. package/dist/components/badge/Badge.d.ts +1 -1
  8. package/dist/components/badge/{Badge2.js → Badge.js} +1 -2
  9. package/dist/components/badge/Badge.js.map +1 -0
  10. package/dist/components/badge/index.css +115 -0
  11. package/dist/components/badge/index.js +1 -1
  12. package/dist/components/button/index.css +295 -0
  13. package/dist/components/checkbox/index.css +181 -0
  14. package/dist/components/choicebox/Choicebox.d.ts +21 -0
  15. package/dist/components/choicebox/Choicebox.js +55 -0
  16. package/dist/components/choicebox/Choicebox.js.map +1 -0
  17. package/dist/components/choicebox/index.css +364 -0
  18. package/dist/components/choicebox/index.d.ts +1 -0
  19. package/dist/components/choicebox/index.js +2 -0
  20. package/dist/components/close-button/CloseButton.d.ts +1 -1
  21. package/dist/components/close-button/index.css +47 -0
  22. package/dist/components/favourite-button/FavouriteButton.d.ts +15 -0
  23. package/dist/components/favourite-button/FavouriteButton.js +25 -0
  24. package/dist/components/favourite-button/FavouriteButton.js.map +1 -0
  25. package/dist/components/favourite-button/index.css +86 -0
  26. package/dist/components/favourite-button/index.d.ts +2 -0
  27. package/dist/components/favourite-button/index.js +2 -0
  28. package/dist/components/index.css +12 -0
  29. package/dist/components/index.d.ts +12 -0
  30. package/dist/components/link/Link.d.ts +11 -0
  31. package/dist/components/link/Link.js +65 -0
  32. package/dist/components/link/Link.js.map +1 -0
  33. package/dist/components/link/index.css +122 -0
  34. package/dist/components/link/index.d.ts +1 -0
  35. package/dist/components/link/index.js +2 -0
  36. package/dist/components/nav-pill/NavPill.d.ts +21 -0
  37. package/dist/components/nav-pill/NavPill.js +54 -0
  38. package/dist/components/nav-pill/NavPill.js.map +1 -0
  39. package/dist/components/nav-pill/index.css +96 -0
  40. package/dist/components/nav-pill/index.d.ts +1 -0
  41. package/dist/components/nav-pill/index.js +2 -0
  42. package/dist/components/pagination/Pagination.d.ts +32 -0
  43. package/dist/components/pagination/Pagination.js +100 -0
  44. package/dist/components/pagination/Pagination.js.map +1 -0
  45. package/dist/components/pagination/index.css +139 -0
  46. package/dist/components/pagination/index.d.ts +1 -0
  47. package/dist/components/pagination/index.js +2 -0
  48. package/dist/components/pagination/pagination.helpers.d.ts +26 -0
  49. package/dist/components/pagination/pagination.helpers.js +136 -0
  50. package/dist/components/pagination/pagination.helpers.js.map +1 -0
  51. package/dist/components/progress-bar/ProgressBar.d.ts +35 -0
  52. package/dist/components/progress-bar/ProgressBar.js +86 -0
  53. package/dist/components/progress-bar/ProgressBar.js.map +1 -0
  54. package/dist/components/progress-bar/index.css +193 -0
  55. package/dist/components/progress-bar/index.d.ts +1 -0
  56. package/dist/components/progress-bar/index.js +2 -0
  57. package/dist/components/radio/index.css +133 -0
  58. package/dist/index.css +1101 -150
  59. package/dist/index.js +8 -2
  60. package/{tokens → dist/tokens}/css/colors.css +7 -4
  61. package/{tokens → dist/tokens}/css/primitives.css +1 -1
  62. package/{tokens → dist/tokens}/scss/_colors.scss +8 -5
  63. package/{tokens → dist/tokens}/scss/_index_css_vars.scss +3 -0
  64. package/{tokens → dist/tokens}/scss/_primitives.scss +1 -1
  65. package/{tokens → dist/tokens}/scss/_typography.scss +1 -1
  66. package/package.json +16 -7
  67. package/dist/components/badge/Badge2.js.map +0 -1
  68. /package/{tokens → dist/tokens}/css/borders.css +0 -0
  69. /package/{tokens → dist/tokens}/css/breakpoints.css +0 -0
  70. /package/{tokens → dist/tokens}/css/index.css +0 -0
  71. /package/{tokens → dist/tokens}/css/shadows.css +0 -0
  72. /package/{tokens → dist/tokens}/css/sizes.css +0 -0
  73. /package/{tokens → dist/tokens}/css/spacing.css +0 -0
  74. /package/{tokens → dist/tokens}/css/typography.css +0 -0
  75. /package/{tokens → dist/tokens}/scss/_borders.scss +0 -0
  76. /package/{tokens → dist/tokens}/scss/_breakpoints.scss +0 -0
  77. /package/{tokens → dist/tokens}/scss/_index.legacy.scss +0 -0
  78. /package/{tokens → dist/tokens}/scss/_index.scss +0 -0
  79. /package/{tokens → dist/tokens}/scss/_shadows.scss +0 -0
  80. /package/{tokens → dist/tokens}/scss/_sizes.scss +0 -0
  81. /package/{tokens → dist/tokens}/scss/_spacing.scss +0 -0
@@ -0,0 +1,15 @@
1
+ import { ButtonHTMLAttributes } from 'react';
2
+ export interface FavouriteButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {
3
+ /**
4
+ * Whether the item is currently selected as a favourite.
5
+ * Controls the filled/outlined icon state and `aria-pressed`.
6
+ */
7
+ selected?: boolean;
8
+ /**
9
+ * Accessible name announced by screen readers.
10
+ * Must be a translated string provided by the caller.
11
+ * Typically "Add to favourites" or "Remove from favourites".
12
+ */
13
+ 'aria-label': string;
14
+ }
15
+ export declare const FavouriteButton: import('react').ForwardRefExoticComponent<FavouriteButtonProps & import('react').RefAttributes<HTMLButtonElement>>;
@@ -0,0 +1,25 @@
1
+ import { forwardRef } from "react";
2
+ import { jsx } from "react/jsx-runtime";
3
+ //#region components/favourite-button/FavouriteButton.tsx
4
+ var FavouriteButton = forwardRef(function FavouriteButton({ selected = false, className, "aria-label": ariaLabel, ...props }, ref) {
5
+ return /* @__PURE__ */ jsx("button", {
6
+ ref,
7
+ className: [
8
+ "mds-favourite-button",
9
+ selected ? "mds-favourite-button--selected" : null,
10
+ className
11
+ ].filter(Boolean).join(" "),
12
+ type: "button",
13
+ "aria-label": ariaLabel,
14
+ "aria-pressed": selected,
15
+ ...props,
16
+ children: /* @__PURE__ */ jsx("span", {
17
+ className: "mds-favourite-button__icon",
18
+ "aria-hidden": "true"
19
+ })
20
+ });
21
+ });
22
+ //#endregion
23
+ export { FavouriteButton };
24
+
25
+ //# sourceMappingURL=FavouriteButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FavouriteButton.js","names":[],"sources":["../../../components/favourite-button/FavouriteButton.tsx"],"sourcesContent":["import type { ButtonHTMLAttributes } from 'react';\nimport { forwardRef } from 'react';\n\nexport interface FavouriteButtonProps extends Omit<\n ButtonHTMLAttributes<HTMLButtonElement>,\n 'type'\n> {\n /**\n * Whether the item is currently selected as a favourite.\n * Controls the filled/outlined icon state and `aria-pressed`.\n */\n selected?: boolean;\n\n /**\n * Accessible name announced by screen readers.\n * Must be a translated string provided by the caller.\n * Typically \"Add to favourites\" or \"Remove from favourites\".\n */\n 'aria-label': string;\n}\n\nexport const FavouriteButton = forwardRef<\n HTMLButtonElement,\n FavouriteButtonProps\n>(function FavouriteButton(\n { selected = false, className, 'aria-label': ariaLabel, ...props },\n ref,\n) {\n const classes = [\n 'mds-favourite-button',\n selected ? 'mds-favourite-button--selected' : null,\n className,\n ]\n .filter(Boolean)\n .join(' ');\n\n return (\n <button\n ref={ref}\n className={classes}\n type=\"button\"\n aria-label={ariaLabel}\n aria-pressed={selected}\n {...props}\n >\n <span className=\"mds-favourite-button__icon\" aria-hidden=\"true\" />\n </button>\n );\n});\n"],"mappings":";;;AAqBA,IAAa,kBAAkB,WAG7B,SAAS,gBACT,EAAE,WAAW,OAAO,WAAW,cAAc,WAAW,GAAG,SAC3D,KACA;CASA,OACE,oBAAC,UAAD;EACO;EACL,WAXY;GACd;GACA,WAAW,mCAAmC;GAC9C;EACF,EACG,OAAO,OAAO,EACd,KAAK,GAKO;EACX,MAAK;EACL,cAAY;EACZ,gBAAc;EACd,GAAI;YAEJ,oBAAC,QAAD;GAAM,WAAU;GAA6B,eAAY;EAAQ,CAAA;CAC3D,CAAA;AAEZ,CAAC"}
@@ -0,0 +1,86 @@
1
+ /* Favourite button (icon-only toggle) */
2
+
3
+ .mds-favourite-button {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ padding: var(--mds-spacing-xs);
8
+ border: none;
9
+ border-radius: var(--mds-border-radius-pill);
10
+ background-color: var(--mds-bg-surface-default);
11
+ color: var(--mds-text-default);
12
+ cursor: pointer;
13
+ transition:
14
+ background-color 0.15s ease-in-out,
15
+ color 0.15s ease-in-out;
16
+ }
17
+
18
+ .mds-favourite-button__icon {
19
+ display: block;
20
+ inline-size: var(--mds-icons-xs);
21
+ block-size: var(--mds-icons-xs);
22
+ background-color: currentColor;
23
+ mask-image: url('./assets/star-outline.svg');
24
+ mask-repeat: no-repeat;
25
+ mask-position: center;
26
+ mask-size: contain;
27
+ -webkit-mask-image: url('./assets/star-outline.svg');
28
+ -webkit-mask-repeat: no-repeat;
29
+ -webkit-mask-position: center;
30
+ -webkit-mask-size: contain;
31
+ }
32
+
33
+ .mds-favourite-button--selected .mds-favourite-button__icon {
34
+ mask-image: url('./assets/star-filled.svg');
35
+ -webkit-mask-image: url('./assets/star-filled.svg');
36
+ }
37
+
38
+ /* Unselected states */
39
+ .mds-favourite-button:hover:not(:disabled) {
40
+ background-color: var(--mds-bg-surface-subtle);
41
+ color: var(--mds-text-subtle);
42
+ }
43
+
44
+ .mds-favourite-button:active:not(:disabled) {
45
+ background-color: var(--mds-bg-surface-strong);
46
+ color: var(--mds-text-default);
47
+ }
48
+
49
+ /* Selected states */
50
+ .mds-favourite-button--selected {
51
+ background-color: var(--mds-bg-surface-default);
52
+ color: var(--mds-text-link-primary-default);
53
+ }
54
+
55
+ .mds-favourite-button--selected:hover:not(:disabled) {
56
+ background-color: var(--mds-bg-surface-subtle);
57
+ color: var(--mds-text-link-primary-hover);
58
+ }
59
+
60
+ .mds-favourite-button--selected:active:not(:disabled) {
61
+ background-color: var(--mds-bg-surface-strong);
62
+ color: var(--mds-text-link-primary-default);
63
+ }
64
+
65
+ /* Disabled states */
66
+ .mds-favourite-button:disabled {
67
+ background-color: var(--mds-bg-surface-default);
68
+ color: var(--mds-text-muted);
69
+ cursor: not-allowed;
70
+ }
71
+
72
+ .mds-favourite-button--selected:disabled {
73
+ color: var(--mds-text-link-primary-disabled);
74
+ }
75
+
76
+ /* Focus ring */
77
+ .mds-favourite-button:focus {
78
+ box-shadow: none;
79
+ outline: none;
80
+ }
81
+
82
+ .mds-favourite-button:focus-visible {
83
+ box-shadow: none;
84
+ outline: var(--mds-stroke-weight-md) solid var(--mds-focus-default);
85
+ outline-offset: 0;
86
+ }
@@ -0,0 +1,2 @@
1
+ export { FavouriteButton } from './FavouriteButton';
2
+ export type { FavouriteButtonProps } from './FavouriteButton';
@@ -0,0 +1,2 @@
1
+ import { FavouriteButton } from "./FavouriteButton.js";
2
+ export { FavouriteButton };
@@ -0,0 +1,12 @@
1
+ @import './activity-icon/index.css';
2
+ @import './badge/index.css';
3
+ @import './button/index.css';
4
+ @import './checkbox/index.css';
5
+ @import './choicebox/index.css';
6
+ @import './close-button/index.css';
7
+ @import './favourite-button/index.css';
8
+ @import './link/index.css';
9
+ @import './nav-pill/index.css';
10
+ @import './pagination/index.css';
11
+ @import './progress-bar/index.css';
12
+ @import './radio/index.css';
@@ -6,7 +6,19 @@ export { Button } from './button';
6
6
  export type { ButtonProps } from './button';
7
7
  export { Checkbox } from './checkbox';
8
8
  export type { CheckboxProps } from './checkbox';
9
+ export { Choicebox } from './choicebox';
10
+ export type { ChoiceboxProps } from './choicebox';
9
11
  export { CloseButton } from './close-button';
10
12
  export type { CloseButtonProps } from './close-button';
13
+ export { FavouriteButton } from './favourite-button';
14
+ export type { FavouriteButtonProps } from './favourite-button';
15
+ export { Link } from './link';
16
+ export type { LinkProps } from './link';
17
+ export { NavPill } from './nav-pill';
18
+ export type { NavPillProps } from './nav-pill';
19
+ export { Pagination } from './pagination';
20
+ export type { PaginationProps } from './pagination';
21
+ export { ProgressBar } from './progress-bar';
22
+ export type { ProgressBarProps } from './progress-bar';
11
23
  export { Radio } from './radio';
12
24
  export type { RadioProps } from './radio';
@@ -0,0 +1,11 @@
1
+ import { AnchorHTMLAttributes, ReactElement } from 'react';
2
+ type IconElement = ReactElement<'i' | 'svg'>;
3
+ export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
4
+ label: string;
5
+ variant?: string;
6
+ disabled?: boolean;
7
+ startIcon?: IconElement;
8
+ endIcon?: IconElement;
9
+ }
10
+ export declare const Link: import('react').ForwardRefExoticComponent<LinkProps & import('react').RefAttributes<HTMLAnchorElement>>;
11
+ export {};
@@ -0,0 +1,65 @@
1
+ import { forwardRef, isValidElement } from "react";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region components/link/Link.tsx
4
+ var allowedVariants = ["primary", "secondary"];
5
+ var isIconElement = (el, propName) => {
6
+ return isValidElement(el) && (el.type === "i" || el.type === "svg");
7
+ };
8
+ var Link = forwardRef(function Link({ label, variant, disabled = false, startIcon, endIcon, className, href, target, rel, onClick, tabIndex, role, ...props }, ref) {
9
+ const resolvedVariant = variant && allowedVariants.includes(variant) ? variant : "primary";
10
+ const resolvedStartIcon = isIconElement(startIcon, "startIcon") ? startIcon : null;
11
+ const resolvedEndIcon = isIconElement(endIcon, "endIcon") ? endIcon : null;
12
+ const handleClick = (event) => {
13
+ if (disabled) {
14
+ event.preventDefault();
15
+ event.stopPropagation();
16
+ return;
17
+ }
18
+ onClick?.(event);
19
+ };
20
+ const resolvedRel = (() => {
21
+ if (target !== "_blank") return rel;
22
+ const parts = new Set([
23
+ ...(rel ?? "").split(/\s+/).filter(Boolean),
24
+ "noopener",
25
+ "noreferrer"
26
+ ]);
27
+ return Array.from(parts).join(" ");
28
+ })();
29
+ const classes = ["mds-link", `mds-link--${resolvedVariant}`];
30
+ if (disabled) classes.push("mds-link--disabled");
31
+ if (className) classes.push(className);
32
+ return /* @__PURE__ */ jsxs("a", {
33
+ ref,
34
+ ...props,
35
+ className: classes.join(" "),
36
+ href: disabled ? void 0 : href,
37
+ target,
38
+ rel: resolvedRel,
39
+ "aria-disabled": disabled || void 0,
40
+ tabIndex: disabled ? -1 : tabIndex,
41
+ role: disabled ? role ?? "link" : role,
42
+ onClick: handleClick,
43
+ children: [
44
+ resolvedStartIcon ? /* @__PURE__ */ jsx("span", {
45
+ className: "mds-link__icon",
46
+ "aria-hidden": "true",
47
+ children: resolvedStartIcon
48
+ }) : null,
49
+ /* @__PURE__ */ jsx("span", {
50
+ className: "mds-link__label",
51
+ children: label
52
+ }),
53
+ resolvedStartIcon ? null : resolvedEndIcon ? /* @__PURE__ */ jsx("span", {
54
+ className: "mds-link__icon",
55
+ "aria-hidden": "true",
56
+ children: resolvedEndIcon
57
+ }) : null
58
+ ]
59
+ });
60
+ });
61
+ Link.displayName = "Link";
62
+ //#endregion
63
+ export { Link };
64
+
65
+ //# sourceMappingURL=Link.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Link.js","names":[],"sources":["../../../components/link/Link.tsx"],"sourcesContent":["import type { AnchorHTMLAttributes, MouseEvent, ReactElement } from 'react';\nimport { forwardRef, isValidElement } from 'react';\n\ntype LinkVariant = 'primary' | 'secondary';\n\ntype IconElement = ReactElement<'i' | 'svg'>;\n\nexport interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {\n label: string;\n variant?: string;\n disabled?: boolean;\n startIcon?: IconElement;\n endIcon?: IconElement;\n}\n\nconst allowedVariants: LinkVariant[] = ['primary', 'secondary'];\n\nconst isIconElement = (el: unknown, propName: string): el is IconElement => {\n const valid = isValidElement(el) && (el.type === 'i' || el.type === 'svg');\n if (!valid && el != null && import.meta.env.DEV) {\n console.error(`Link: \\`${propName}\\` must be an <i> or <svg> element.`);\n }\n return valid;\n};\n\nexport const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(\n {\n label,\n variant,\n disabled = false,\n startIcon,\n endIcon,\n className,\n href,\n target,\n rel,\n onClick,\n tabIndex,\n role,\n ...props\n },\n ref,\n) {\n const resolvedVariant =\n variant && allowedVariants.includes(variant as LinkVariant)\n ? (variant as LinkVariant)\n : 'primary';\n const resolvedStartIcon = isIconElement(startIcon, 'startIcon')\n ? startIcon\n : null;\n const resolvedEndIcon = isIconElement(endIcon, 'endIcon') ? endIcon : null;\n\n if (import.meta.env.DEV) {\n const hasAccessibleName =\n label.trim().length > 0 ||\n Boolean(props['aria-label']?.trim()) ||\n Boolean(props['aria-labelledby']?.trim());\n\n if (!hasAccessibleName) {\n console.warn(\n 'Link: provide a label, aria-label, or aria-labelledby for accessibility.',\n );\n }\n\n if (variant && !allowedVariants.includes(variant as LinkVariant)) {\n console.warn(\n `[MDS Link] Invalid variant \"${variant}\". Falling back to \"primary\". Allowed: ${allowedVariants.join(', ')}`,\n );\n }\n\n if (resolvedStartIcon && resolvedEndIcon) {\n console.warn(\n 'Link: pass either startIcon or endIcon, not both. Rendering startIcon only.',\n );\n }\n }\n\n const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {\n // Anchors have no native disabled state, so block activation explicitly.\n if (disabled) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n\n onClick?.(event);\n };\n\n // When opening in a new tab, ensure the page can't access window.opener\n // and browsers don't send the Referer header (reverse tabnabbing protection).\n const resolvedRel = (() => {\n if (target !== '_blank') return rel;\n const parts = new Set([\n ...(rel ?? '').split(/\\s+/).filter(Boolean),\n 'noopener',\n 'noreferrer',\n ]);\n return Array.from(parts).join(' ');\n })();\n\n const classes = ['mds-link', `mds-link--${resolvedVariant}`];\n if (disabled) {\n classes.push('mds-link--disabled');\n }\n if (className) {\n classes.push(className);\n }\n\n return (\n <a\n ref={ref}\n {...props}\n className={classes.join(' ')}\n href={disabled ? undefined : href}\n target={target}\n rel={resolvedRel}\n aria-disabled={disabled || undefined}\n tabIndex={disabled ? -1 : tabIndex}\n role={disabled ? (role ?? 'link') : role}\n onClick={handleClick}\n >\n {resolvedStartIcon ? (\n <span className=\"mds-link__icon\" aria-hidden=\"true\">\n {resolvedStartIcon}\n </span>\n ) : null}\n <span className=\"mds-link__label\">{label}</span>\n {resolvedStartIcon ? null : resolvedEndIcon ? (\n <span className=\"mds-link__icon\" aria-hidden=\"true\">\n {resolvedEndIcon}\n </span>\n ) : null}\n </a>\n );\n});\n\nLink.displayName = 'Link';\n"],"mappings":";;;AAeA,IAAM,kBAAiC,CAAC,WAAW,WAAW;AAE9D,IAAM,iBAAiB,IAAa,aAAwC;CAK1E,OAJc,eAAe,EAAE,MAAM,GAAG,SAAS,OAAO,GAAG,SAAS;AAKtE;AAEA,IAAa,OAAO,WAAyC,SAAS,KACpE,EACE,OACA,SACA,WAAW,OACX,WACA,SACA,WACA,MACA,QACA,KACA,SACA,UACA,MACA,GAAG,SAEL,KACA;CACA,MAAM,kBACJ,WAAW,gBAAgB,SAAS,OAAsB,IACrD,UACD;CACN,MAAM,oBAAoB,cAAc,WAAW,WAAW,IAC1D,YACA;CACJ,MAAM,kBAAkB,cAAc,SAAS,SAAS,IAAI,UAAU;CA2BtE,MAAM,eAAe,UAAyC;EAE5D,IAAI,UAAU;GACZ,MAAM,eAAe;GACrB,MAAM,gBAAgB;GACtB;EACF;EAEA,UAAU,KAAK;CACjB;CAIA,MAAM,qBAAqB;EACzB,IAAI,WAAW,UAAU,OAAO;EAChC,MAAM,QAAQ,IAAI,IAAI;GACpB,IAAI,OAAO,IAAI,MAAM,KAAK,EAAE,OAAO,OAAO;GAC1C;GACA;EACF,CAAC;EACD,OAAO,MAAM,KAAK,KAAK,EAAE,KAAK,GAAG;CACnC,GAAG;CAEH,MAAM,UAAU,CAAC,YAAY,aAAa,iBAAiB;CAC3D,IAAI,UACF,QAAQ,KAAK,oBAAoB;CAEnC,IAAI,WACF,QAAQ,KAAK,SAAS;CAGxB,OACE,qBAAC,KAAD;EACO;EACL,GAAI;EACJ,WAAW,QAAQ,KAAK,GAAG;EAC3B,MAAM,WAAW,KAAA,IAAY;EACrB;EACR,KAAK;EACL,iBAAe,YAAY,KAAA;EAC3B,UAAU,WAAW,KAAK;EAC1B,MAAM,WAAY,QAAQ,SAAU;EACpC,SAAS;YAVX;GAYG,oBACC,oBAAC,QAAD;IAAM,WAAU;IAAiB,eAAY;cAC1C;GACG,CAAA,IACJ;GACJ,oBAAC,QAAD;IAAM,WAAU;cAAmB;GAAY,CAAA;GAC9C,oBAAoB,OAAO,kBAC1B,oBAAC,QAAD;IAAM,WAAU;IAAiB,eAAY;cAC1C;GACG,CAAA,IACJ;EACH;;AAEP,CAAC;AAED,KAAK,cAAc"}
@@ -0,0 +1,122 @@
1
+ .mds-link {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: var(--mds-spacing-xs);
5
+
6
+ color: var(--mds-text-link-primary-default);
7
+ font-family: var(--mds-font-family-base);
8
+ font-weight: var(--mds-font-weight-regular);
9
+ font-size: var(--mds-font-size-paragraph-default);
10
+ line-height: var(--mds-line-height-paragraph-xs);
11
+ letter-spacing: var(--mds-letter-spacing-default);
12
+ text-decoration: none;
13
+
14
+ cursor: pointer;
15
+ }
16
+
17
+ .mds-link:hover:not(:focus-visible) {
18
+ color: var(--mds-text-link-primary-hover);
19
+ }
20
+
21
+ .mds-link:active {
22
+ color: var(--mds-text-link-primary-hover);
23
+ }
24
+
25
+ /* Underline on hover applies only to the label text, not the icon.
26
+ Pressed state has no underline (Figma). Thickness and position use
27
+ from-font so the underline follows the font's own metrics. */
28
+ .mds-link:hover:not(:focus-visible) .mds-link__label {
29
+ text-decoration: underline;
30
+ text-decoration-thickness: from-font;
31
+ text-underline-position: from-font;
32
+ }
33
+
34
+ /* Slide icon toward the label on hover using transform so the link node width
35
+ stays constant (gap remains --mds-spacing-xs; only the icon moves visually). */
36
+ .mds-link:hover:not(:focus-visible) .mds-link__icon:first-child {
37
+ transform: translateX(var(--mds-spacing-xxs));
38
+ }
39
+
40
+ .mds-link:hover:not(:focus-visible) .mds-link__icon:last-child {
41
+ transform: translateX(calc(-1 * var(--mds-spacing-xxs)));
42
+ }
43
+
44
+ [dir='rtl'] .mds-link:hover:not(:focus-visible) .mds-link__icon:first-child {
45
+ transform: translateX(calc(-1 * var(--mds-spacing-xxs)));
46
+ }
47
+
48
+ [dir='rtl'] .mds-link:hover:not(:focus-visible) .mds-link__icon:last-child {
49
+ transform: translateX(var(--mds-spacing-xxs));
50
+ }
51
+
52
+ /* Reset icon position on press */
53
+ .mds-link:active .mds-link__icon {
54
+ transform: none;
55
+ }
56
+
57
+ .mds-link:focus {
58
+ outline: none;
59
+ }
60
+
61
+ .mds-link:focus-visible {
62
+ /* Bottom-only focus underline matching Figma: 2px border-bottom, 1px gap below content */
63
+ outline: none;
64
+ border-radius: 0;
65
+ padding-bottom: var(--mds-spacing-offset);
66
+ border-bottom: var(--mds-stroke-weight-md) solid var(--mds-focus-default);
67
+ }
68
+
69
+ .mds-link.mds-link--secondary {
70
+ color: var(--mds-text-subtle);
71
+ }
72
+
73
+ .mds-link.mds-link--secondary:hover:not(:focus-visible),
74
+ .mds-link.mds-link--secondary:active {
75
+ color: var(--mds-text-default);
76
+ }
77
+
78
+ .mds-link.mds-link--disabled,
79
+ .mds-link.mds-link--disabled:hover,
80
+ .mds-link.mds-link--disabled:active {
81
+ color: var(--mds-text-link-primary-disabled);
82
+ pointer-events: none;
83
+ }
84
+
85
+ .mds-link.mds-link--disabled .mds-link__icon,
86
+ .mds-link.mds-link--disabled:hover .mds-link__icon,
87
+ .mds-link.mds-link--disabled:active .mds-link__icon {
88
+ transform: none;
89
+ }
90
+
91
+ .mds-link.mds-link--disabled .mds-link__label,
92
+ .mds-link.mds-link--disabled:hover .mds-link__label,
93
+ .mds-link.mds-link--disabled:active .mds-link__label {
94
+ text-decoration: none;
95
+ }
96
+
97
+ .mds-link.mds-link--secondary.mds-link--disabled,
98
+ .mds-link.mds-link--secondary.mds-link--disabled:hover,
99
+ .mds-link.mds-link--secondary.mds-link--disabled:active {
100
+ color: var(--mds-text-muted);
101
+ }
102
+
103
+ .mds-link__icon,
104
+ .mds-link__icon i,
105
+ .mds-link__icon svg {
106
+ inline-size: var(--mds-icons-xs);
107
+ block-size: var(--mds-icons-xs);
108
+ font-size: var(--mds-icons-xs);
109
+ line-height: 1;
110
+ flex-shrink: 0;
111
+ }
112
+
113
+ .mds-link__icon {
114
+ display: inline-flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ transition: transform 150ms ease;
118
+ }
119
+
120
+ .mds-link__label {
121
+ text-align: start;
122
+ }
@@ -0,0 +1 @@
1
+ export * from './Link';
@@ -0,0 +1,2 @@
1
+ import { Link } from "./Link.js";
2
+ export { Link };
@@ -0,0 +1,21 @@
1
+ import { AnchorHTMLAttributes } from 'react';
2
+ export interface NavPillProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'aria-current' | 'aria-disabled' | 'role'> {
3
+ /**
4
+ * Visible label text. Must be a caller-supplied translated string.
5
+ */
6
+ label: string;
7
+ /**
8
+ * Whether this pill is currently the active/selected navigation item.
9
+ * Controls the active-indicator dot and selected visual styles.
10
+ */
11
+ selected?: boolean;
12
+ /**
13
+ * Destination URL for the navigation pill.
14
+ */
15
+ href: string;
16
+ /**
17
+ * Marks the anchor as non-interactive while preserving anchor semantics.
18
+ */
19
+ disabled?: boolean;
20
+ }
21
+ export declare const NavPill: import('react').ForwardRefExoticComponent<NavPillProps & import('react').RefAttributes<HTMLAnchorElement>>;
@@ -0,0 +1,54 @@
1
+ import { forwardRef } from "react";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region components/nav-pill/NavPill.tsx
4
+ var NavPill = forwardRef(function NavPill({ label, selected = false, className, disabled, onClick, href, target, rel, tabIndex, ...props }, ref) {
5
+ const isDisabled = selected ? false : disabled;
6
+ const classes = [
7
+ "mds-nav-pill",
8
+ selected ? "mds-nav-pill--selected" : null,
9
+ className
10
+ ].filter(Boolean).join(" ");
11
+ const resolvedHref = isDisabled ? void 0 : href;
12
+ const resolvedRel = (() => {
13
+ if (target !== "_blank") return rel;
14
+ const parts = new Set([
15
+ ...(rel ?? "").split(/\s+/).filter(Boolean),
16
+ "noopener",
17
+ "noreferrer"
18
+ ]);
19
+ return Array.from(parts).join(" ");
20
+ })();
21
+ const handleClick = (event) => {
22
+ if (isDisabled) {
23
+ event.preventDefault();
24
+ event.stopPropagation();
25
+ return;
26
+ }
27
+ onClick?.(event);
28
+ };
29
+ return /* @__PURE__ */ jsxs("a", {
30
+ ref,
31
+ ...props,
32
+ className: classes,
33
+ target,
34
+ rel: resolvedRel,
35
+ href: resolvedHref,
36
+ "aria-disabled": isDisabled ? "true" : void 0,
37
+ tabIndex: isDisabled ? -1 : tabIndex,
38
+ role: isDisabled ? "link" : void 0,
39
+ onClick: handleClick,
40
+ "aria-current": selected ? "page" : void 0,
41
+ children: [selected && /* @__PURE__ */ jsx("span", {
42
+ className: "mds-nav-pill__indicator",
43
+ "aria-hidden": "true"
44
+ }), /* @__PURE__ */ jsx("span", {
45
+ className: "mds-nav-pill__label",
46
+ children: label
47
+ })]
48
+ });
49
+ });
50
+ NavPill.displayName = "NavPill";
51
+ //#endregion
52
+ export { NavPill };
53
+
54
+ //# sourceMappingURL=NavPill.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NavPill.js","names":[],"sources":["../../../components/nav-pill/NavPill.tsx"],"sourcesContent":["import type { AnchorHTMLAttributes, MouseEvent } from 'react';\nimport { forwardRef } from 'react';\n\nexport interface NavPillProps extends Omit<\n AnchorHTMLAttributes<HTMLAnchorElement>,\n 'href' | 'aria-current' | 'aria-disabled' | 'role'\n> {\n /**\n * Visible label text. Must be a caller-supplied translated string.\n */\n label: string;\n\n /**\n * Whether this pill is currently the active/selected navigation item.\n * Controls the active-indicator dot and selected visual styles.\n */\n selected?: boolean;\n\n /**\n * Destination URL for the navigation pill.\n */\n href: string;\n\n /**\n * Marks the anchor as non-interactive while preserving anchor semantics.\n */\n disabled?: boolean;\n}\n\nexport const NavPill = forwardRef<HTMLAnchorElement, NavPillProps>(\n function NavPill(\n {\n label,\n selected = false,\n className,\n disabled,\n onClick,\n href,\n target,\n rel,\n tabIndex,\n ...props\n },\n ref,\n ) {\n // A selected pill represents the active navigation destination and cannot\n // be disabled — disabling it would leave users with no way to identify\n // the current location. Silently ignore the disabled prop when selected.\n if (import.meta.env.DEV && selected && disabled) {\n console.warn(\n '[MDS NavPill] A selected NavPill cannot be disabled. ' +\n 'The disabled prop is ignored when selected=true.',\n );\n }\n const isDisabled = selected ? false : disabled;\n\n const classes = [\n 'mds-nav-pill',\n selected ? 'mds-nav-pill--selected' : null,\n className,\n ]\n .filter(Boolean)\n .join(' ');\n\n const resolvedHref = isDisabled ? undefined : href;\n\n // Align anchor hardening with Link: when opening in a new tab, enforce\n // noopener+noreferrer to prevent opener access and referrer leakage.\n const resolvedRel = (() => {\n if (target !== '_blank') return rel;\n const parts = new Set([\n ...(rel ?? '').split(/\\s+/).filter(Boolean),\n 'noopener',\n 'noreferrer',\n ]);\n return Array.from(parts).join(' ');\n })();\n\n const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {\n if (isDisabled) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n\n onClick?.(event);\n };\n\n return (\n <a\n ref={ref}\n {...props}\n className={classes}\n target={target}\n rel={resolvedRel}\n href={resolvedHref}\n aria-disabled={isDisabled ? 'true' : undefined}\n tabIndex={isDisabled ? -1 : tabIndex}\n role={isDisabled ? 'link' : undefined}\n onClick={handleClick}\n aria-current={selected ? 'page' : undefined}\n >\n {/* Active-indicator dot: only visible when selected */}\n {selected && (\n <span className=\"mds-nav-pill__indicator\" aria-hidden=\"true\" />\n )}\n <span className=\"mds-nav-pill__label\">{label}</span>\n </a>\n );\n },\n);\n\nNavPill.displayName = 'NavPill';\n"],"mappings":";;;AA6BA,IAAa,UAAU,WACrB,SAAS,QACP,EACE,OACA,WAAW,OACX,WACA,UACA,SACA,MACA,QACA,KACA,UACA,GAAG,SAEL,KACA;CAUA,MAAM,aAAa,WAAW,QAAQ;CAEtC,MAAM,UAAU;EACd;EACA,WAAW,2BAA2B;EACtC;CACF,EACG,OAAO,OAAO,EACd,KAAK,GAAG;CAEX,MAAM,eAAe,aAAa,KAAA,IAAY;CAI9C,MAAM,qBAAqB;EACzB,IAAI,WAAW,UAAU,OAAO;EAChC,MAAM,QAAQ,IAAI,IAAI;GACpB,IAAI,OAAO,IAAI,MAAM,KAAK,EAAE,OAAO,OAAO;GAC1C;GACA;EACF,CAAC;EACD,OAAO,MAAM,KAAK,KAAK,EAAE,KAAK,GAAG;CACnC,GAAG;CAEH,MAAM,eAAe,UAAyC;EAC5D,IAAI,YAAY;GACd,MAAM,eAAe;GACrB,MAAM,gBAAgB;GACtB;EACF;EAEA,UAAU,KAAK;CACjB;CAEA,OACE,qBAAC,KAAD;EACO;EACL,GAAI;EACJ,WAAW;EACH;EACR,KAAK;EACL,MAAM;EACN,iBAAe,aAAa,SAAS,KAAA;EACrC,UAAU,aAAa,KAAK;EAC5B,MAAM,aAAa,SAAS,KAAA;EAC5B,SAAS;EACT,gBAAc,WAAW,SAAS,KAAA;YAXpC,CAcG,YACC,oBAAC,QAAD;GAAM,WAAU;GAA0B,eAAY;EAAQ,CAAA,GAEhE,oBAAC,QAAD;GAAM,WAAU;aAAuB;EAAY,CAAA,CAClD;;AAEP,CACF;AAEA,QAAQ,cAAc"}
@@ -0,0 +1,96 @@
1
+ /* Nav pill — compact navigation destination indicator */
2
+
3
+ .mds-nav-pill,
4
+ .mds-nav-pill:visited {
5
+ /* Reset anchor/link defaults */
6
+ display: inline-flex;
7
+ align-items: center;
8
+ gap: var(--mds-spacing-xxs);
9
+ padding: var(--mds-spacing-xs) var(--mds-spacing-sm);
10
+ border: none;
11
+ background-color: transparent;
12
+ cursor: pointer;
13
+
14
+ border-radius: var(--mds-border-radius-md);
15
+
16
+ /* Label colour: unselected default */
17
+ color: var(--mds-text-subtle);
18
+ text-decoration: none;
19
+ }
20
+
21
+ .mds-nav-pill:hover,
22
+ .mds-nav-pill:focus,
23
+ .mds-nav-pill:active,
24
+ .mds-nav-pill:visited {
25
+ text-decoration: none;
26
+ }
27
+
28
+ /* ---- Label ---- */
29
+
30
+ .mds-nav-pill__label {
31
+ /* UI small — matches Figma: $font-size-sm / font-weight/medium / line-height/paragraph/xs */
32
+ font-family: var(--mds-font-family-base);
33
+ font-size: var(--mds-font-size-paragraph-small);
34
+ font-weight: var(--mds-font-weight-medium);
35
+ line-height: var(--mds-line-height-paragraph-xs);
36
+ white-space: nowrap;
37
+ }
38
+
39
+ /* ---- Active-indicator dot (selected state only) ---- */
40
+
41
+ .mds-nav-pill__indicator {
42
+ display: block;
43
+ width: 4px;
44
+ height: 4px;
45
+ border-radius: var(--mds-border-radius-pill);
46
+ background-color: var(--mds-bg-interactive-primary-default);
47
+ flex-shrink: 0;
48
+ }
49
+
50
+ /* ---- Hover (unselected) ---- */
51
+
52
+ .mds-nav-pill:hover:not([aria-disabled='true']):not(.mds-nav-pill--selected) {
53
+ background-color: var(--mds-bg-nav-pill-hover);
54
+ }
55
+
56
+ /* ---- Pressed / active (unselected) ---- */
57
+
58
+ .mds-nav-pill:active:not([aria-disabled='true']):not(.mds-nav-pill--selected) {
59
+ background-color: var(--mds-bg-nav-pill-pressed);
60
+ }
61
+
62
+ /* ---- Focus-visible ring — keyboard navigation ---- */
63
+
64
+ .mds-nav-pill:focus {
65
+ outline: none;
66
+ box-shadow: none;
67
+ }
68
+
69
+ .mds-nav-pill:focus-visible {
70
+ outline: var(--mds-stroke-weight-md) solid var(--mds-focus-default);
71
+ outline-offset: var(--mds-spacing-offset);
72
+ box-shadow: none;
73
+ }
74
+
75
+ /* ---- Disabled ---- */
76
+
77
+ .mds-nav-pill[aria-disabled='true'] {
78
+ color: var(--mds-text-muted);
79
+ cursor: not-allowed;
80
+ background-color: transparent;
81
+ }
82
+
83
+ /* ---- Selected state ---- */
84
+
85
+ .mds-nav-pill--selected {
86
+ color: var(--mds-text-default);
87
+ background-color: var(--mds-bg-nav-pill-selected);
88
+ }
89
+
90
+ .mds-nav-pill--selected:visited {
91
+ color: var(--mds-text-default);
92
+ }
93
+
94
+ .mds-nav-pill--selected:hover:not([aria-disabled='true']) {
95
+ background-color: var(--mds-bg-nav-pill-selected);
96
+ }
@@ -0,0 +1 @@
1
+ export * from './NavPill';
@@ -0,0 +1,2 @@
1
+ import { NavPill } from "./NavPill.js";
2
+ export { NavPill };
@@ -0,0 +1,32 @@
1
+ import { ComponentPropsWithoutRef } from 'react';
2
+ import { PageLabelFormatter } from './pagination.helpers';
3
+ export interface PaginationProps extends ComponentPropsWithoutRef<'nav'> {
4
+ /** Total number of pages */
5
+ totalPages: number;
6
+ /** Current page number (1-indexed) */
7
+ currentPage: number;
8
+ /** Callback fired when the page changes */
9
+ onPageChange: (page: number) => void;
10
+ /** Accessible name for the pagination landmark. */
11
+ ariaLabel?: string;
12
+ /** Accessible label used for the previous-page button. */
13
+ previousPageLabel?: string;
14
+ /** Accessible label used for the next-page button. */
15
+ nextPageLabel?: string;
16
+ /** Returns the accessible label for each numbered page button. */
17
+ pageLabelFormatter?: PageLabelFormatter;
18
+ /**
19
+ * Controls which variant of pagination to render.
20
+ * Accepts a broad string so JS consumers can be validated at runtime.
21
+ * - `'full'` (default): Shows page numbers between previous and next controls.
22
+ * The visible page count reduces automatically as the viewport width narrows
23
+ * (9 → 7 → 5 items), and collapses to grouped appearance when the viewport
24
+ * is too narrow to fit any page numbers. First and last pages are always shown
25
+ * when needed.
26
+ * - `'grouped'`: Shows only previous and next controls without page numbers.
27
+ */
28
+ variant?: string;
29
+ /** Disables all interactive elements, preventing focus, hover, and page-change events. */
30
+ disabled?: boolean;
31
+ }
32
+ export declare const Pagination: ({ totalPages, currentPage, onPageChange, ariaLabel, previousPageLabel, nextPageLabel, pageLabelFormatter, variant, disabled, className, ...props }: PaginationProps) => import("react").JSX.Element | null;