@moodlehq/design-system 4.1.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 (77) hide show
  1. package/README.md +9 -0
  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/index.css +115 -0
  9. package/dist/components/button/index.css +295 -0
  10. package/dist/components/checkbox/index.css +181 -0
  11. package/dist/components/choicebox/Choicebox.d.ts +21 -0
  12. package/dist/components/choicebox/Choicebox.js +55 -0
  13. package/dist/components/choicebox/Choicebox.js.map +1 -0
  14. package/dist/components/choicebox/index.css +364 -0
  15. package/dist/components/choicebox/index.d.ts +1 -0
  16. package/dist/components/choicebox/index.js +2 -0
  17. package/dist/components/close-button/CloseButton.d.ts +1 -1
  18. package/dist/components/close-button/index.css +47 -0
  19. package/dist/components/favourite-button/FavouriteButton.d.ts +15 -0
  20. package/dist/components/favourite-button/FavouriteButton.js +25 -0
  21. package/dist/components/favourite-button/FavouriteButton.js.map +1 -0
  22. package/dist/components/favourite-button/index.css +86 -0
  23. package/dist/components/favourite-button/index.d.ts +2 -0
  24. package/dist/components/favourite-button/index.js +2 -0
  25. package/dist/components/index.css +12 -0
  26. package/dist/components/index.d.ts +12 -0
  27. package/dist/components/link/Link.d.ts +11 -0
  28. package/dist/components/link/Link.js +65 -0
  29. package/dist/components/link/Link.js.map +1 -0
  30. package/dist/components/link/index.css +122 -0
  31. package/dist/components/link/index.d.ts +1 -0
  32. package/dist/components/link/index.js +2 -0
  33. package/dist/components/nav-pill/NavPill.d.ts +21 -0
  34. package/dist/components/nav-pill/NavPill.js +54 -0
  35. package/dist/components/nav-pill/NavPill.js.map +1 -0
  36. package/dist/components/nav-pill/index.css +96 -0
  37. package/dist/components/nav-pill/index.d.ts +1 -0
  38. package/dist/components/nav-pill/index.js +2 -0
  39. package/dist/components/pagination/Pagination.d.ts +32 -0
  40. package/dist/components/pagination/Pagination.js +100 -0
  41. package/dist/components/pagination/Pagination.js.map +1 -0
  42. package/dist/components/pagination/index.css +139 -0
  43. package/dist/components/pagination/index.d.ts +1 -0
  44. package/dist/components/pagination/index.js +2 -0
  45. package/dist/components/pagination/pagination.helpers.d.ts +26 -0
  46. package/dist/components/pagination/pagination.helpers.js +136 -0
  47. package/dist/components/pagination/pagination.helpers.js.map +1 -0
  48. package/dist/components/progress-bar/ProgressBar.d.ts +35 -0
  49. package/dist/components/progress-bar/ProgressBar.js +86 -0
  50. package/dist/components/progress-bar/ProgressBar.js.map +1 -0
  51. package/dist/components/progress-bar/index.css +193 -0
  52. package/dist/components/progress-bar/index.d.ts +1 -0
  53. package/dist/components/progress-bar/index.js +2 -0
  54. package/dist/components/radio/index.css +133 -0
  55. package/dist/index.css +1066 -0
  56. package/dist/index.js +7 -1
  57. package/{tokens → dist/tokens}/css/colors.css +3 -0
  58. package/{tokens → dist/tokens}/scss/_colors.scss +3 -0
  59. package/{tokens → dist/tokens}/scss/_index_css_vars.scss +3 -0
  60. package/package.json +15 -7
  61. /package/{tokens → dist/tokens}/css/borders.css +0 -0
  62. /package/{tokens → dist/tokens}/css/breakpoints.css +0 -0
  63. /package/{tokens → dist/tokens}/css/index.css +0 -0
  64. /package/{tokens → dist/tokens}/css/primitives.css +0 -0
  65. /package/{tokens → dist/tokens}/css/shadows.css +0 -0
  66. /package/{tokens → dist/tokens}/css/sizes.css +0 -0
  67. /package/{tokens → dist/tokens}/css/spacing.css +0 -0
  68. /package/{tokens → dist/tokens}/css/typography.css +0 -0
  69. /package/{tokens → dist/tokens}/scss/_borders.scss +0 -0
  70. /package/{tokens → dist/tokens}/scss/_breakpoints.scss +0 -0
  71. /package/{tokens → dist/tokens}/scss/_index.legacy.scss +0 -0
  72. /package/{tokens → dist/tokens}/scss/_index.scss +0 -0
  73. /package/{tokens → dist/tokens}/scss/_primitives.scss +0 -0
  74. /package/{tokens → dist/tokens}/scss/_shadows.scss +0 -0
  75. /package/{tokens → dist/tokens}/scss/_sizes.scss +0 -0
  76. /package/{tokens → dist/tokens}/scss/_spacing.scss +0 -0
  77. /package/{tokens → dist/tokens}/scss/_typography.scss +0 -0
@@ -0,0 +1,100 @@
1
+ import { calculateVisiblePageNumbers, resolvePaginationInputs, useViewportMaxVisible } from "./pagination.helpers.js";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ //#region components/pagination/Pagination.tsx
5
+ var MAX_VISIBLE_ELEMENTS = 9;
6
+ var Pagination = ({ totalPages, currentPage, onPageChange, ariaLabel = "Pagination", previousPageLabel = "Previous page", nextPageLabel = "Next page", pageLabelFormatter, variant = "full", disabled = false, className, ...props }) => {
7
+ const { resolvedVariant, resolvedPageLabelFormatter, sanitizedTotalPages, sanitizedCurrentPage } = resolvePaginationInputs(variant, pageLabelFormatter, totalPages, currentPage);
8
+ const viewportMaxVisible = useViewportMaxVisible();
9
+ const [pendingCurrentPage, setPendingCurrentPage] = useState(null);
10
+ const adaptiveResult = resolvedVariant === "full" ? viewportMaxVisible : MAX_VISIBLE_ELEMENTS;
11
+ const effectiveVariant = resolvedVariant === "full" && adaptiveResult === null ? "grouped" : resolvedVariant;
12
+ const maxVisible = adaptiveResult ?? MAX_VISIBLE_ELEMENTS;
13
+ const validCurrentPage = Math.max(1, Math.min(sanitizedCurrentPage, sanitizedTotalPages));
14
+ const previousValidCurrentPageRef = useRef(validCurrentPage);
15
+ useEffect(() => {
16
+ const didControlledPageChange = previousValidCurrentPageRef.current !== validCurrentPage;
17
+ if (pendingCurrentPage !== null && (pendingCurrentPage === validCurrentPage || didControlledPageChange)) setPendingCurrentPage(null);
18
+ previousValidCurrentPageRef.current = validCurrentPage;
19
+ }, [validCurrentPage, pendingCurrentPage]);
20
+ const visualCurrentPage = pendingCurrentPage ?? validCurrentPage;
21
+ const canGoPrevious = visualCurrentPage > 1;
22
+ const canGoNext = visualCurrentPage < sanitizedTotalPages;
23
+ const { showBoundaryPages, pageNumbers, showLeftEllipsis, showRightEllipsis } = useMemo(() => calculateVisiblePageNumbers(visualCurrentPage, sanitizedTotalPages, maxVisible), [
24
+ visualCurrentPage,
25
+ sanitizedTotalPages,
26
+ maxVisible
27
+ ]);
28
+ if (sanitizedTotalPages < 2) return null;
29
+ const handlePageChange = (page) => {
30
+ if (page !== visualCurrentPage && page >= 1 && page <= sanitizedTotalPages) {
31
+ setPendingCurrentPage(page);
32
+ onPageChange(page);
33
+ }
34
+ };
35
+ const classes = ["mds-pagination", `mds-pagination--${effectiveVariant}`];
36
+ if (className) classes.push(className);
37
+ const renderPageButton = (page) => /* @__PURE__ */ jsx("button", {
38
+ type: "button",
39
+ className: "mds-pagination__page",
40
+ onClick: () => handlePageChange(page),
41
+ disabled,
42
+ "aria-label": resolvedPageLabelFormatter(page),
43
+ "aria-current": page === visualCurrentPage ? "page" : void 0,
44
+ "data-current": page === visualCurrentPage,
45
+ children: page
46
+ }, page);
47
+ return /* @__PURE__ */ jsxs("nav", {
48
+ className: classes.join(" "),
49
+ "aria-label": ariaLabel,
50
+ ...props,
51
+ children: [
52
+ /* @__PURE__ */ jsx("button", {
53
+ type: "button",
54
+ className: "mds-pagination__button mds-pagination__button--prev",
55
+ onClick: () => handlePageChange(visualCurrentPage - 1),
56
+ disabled: disabled || !canGoPrevious,
57
+ "aria-label": previousPageLabel,
58
+ tabIndex: !disabled && canGoPrevious ? 0 : -1,
59
+ children: /* @__PURE__ */ jsx("i", {
60
+ className: "fa-solid fa-chevron-left",
61
+ "aria-hidden": "true"
62
+ })
63
+ }),
64
+ effectiveVariant === "full" && /* @__PURE__ */ jsxs("div", {
65
+ className: "mds-pagination__pages",
66
+ children: [
67
+ showBoundaryPages && renderPageButton(1),
68
+ showLeftEllipsis && /* @__PURE__ */ jsx("span", {
69
+ className: "mds-pagination__ellipsis",
70
+ "aria-hidden": "true",
71
+ children: "…"
72
+ }, "left-ellipsis"),
73
+ pageNumbers.map(renderPageButton),
74
+ showRightEllipsis && /* @__PURE__ */ jsx("span", {
75
+ className: "mds-pagination__ellipsis",
76
+ "aria-hidden": "true",
77
+ children: "…"
78
+ }, "right-ellipsis"),
79
+ showBoundaryPages && renderPageButton(sanitizedTotalPages)
80
+ ]
81
+ }),
82
+ /* @__PURE__ */ jsx("button", {
83
+ type: "button",
84
+ className: "mds-pagination__button mds-pagination__button--next",
85
+ onClick: () => handlePageChange(visualCurrentPage + 1),
86
+ disabled: disabled || !canGoNext,
87
+ "aria-label": nextPageLabel,
88
+ tabIndex: !disabled && canGoNext ? 0 : -1,
89
+ children: /* @__PURE__ */ jsx("i", {
90
+ className: "fa-solid fa-chevron-right",
91
+ "aria-hidden": "true"
92
+ })
93
+ })
94
+ ]
95
+ });
96
+ };
97
+ //#endregion
98
+ export { Pagination };
99
+
100
+ //# sourceMappingURL=Pagination.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Pagination.js","names":[],"sources":["../../../components/pagination/Pagination.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef } from 'react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport {\n calculateVisiblePageNumbers,\n resolvePaginationInputs,\n useViewportMaxVisible,\n type PageLabelFormatter,\n} from './pagination.helpers';\n\nconst MAX_VISIBLE_ELEMENTS = 9;\n\nexport interface PaginationProps extends ComponentPropsWithoutRef<'nav'> {\n /** Total number of pages */\n totalPages: number;\n\n /** Current page number (1-indexed) */\n currentPage: number;\n\n /** Callback fired when the page changes */\n onPageChange: (page: number) => void;\n\n /** Accessible name for the pagination landmark. */\n ariaLabel?: string;\n\n /** Accessible label used for the previous-page button. */\n previousPageLabel?: string;\n\n /** Accessible label used for the next-page button. */\n nextPageLabel?: string;\n\n /** Returns the accessible label for each numbered page button. */\n pageLabelFormatter?: PageLabelFormatter;\n\n /**\n * Controls which variant of pagination to render.\n * Accepts a broad string so JS consumers can be validated at runtime.\n * - `'full'` (default): Shows page numbers between previous and next controls.\n * The visible page count reduces automatically as the viewport width narrows\n * (9 → 7 → 5 items), and collapses to grouped appearance when the viewport\n * is too narrow to fit any page numbers. First and last pages are always shown\n * when needed.\n * - `'grouped'`: Shows only previous and next controls without page numbers.\n */\n variant?: string;\n\n /** Disables all interactive elements, preventing focus, hover, and page-change events. */\n disabled?: boolean;\n}\n\nexport const Pagination = ({\n totalPages,\n currentPage,\n onPageChange,\n ariaLabel = 'Pagination',\n previousPageLabel = 'Previous page',\n nextPageLabel = 'Next page',\n pageLabelFormatter,\n variant = 'full',\n disabled = false,\n className,\n ...props\n}: PaginationProps) => {\n const {\n resolvedVariant,\n resolvedPageLabelFormatter,\n sanitizedTotalPages,\n sanitizedCurrentPage,\n } = resolvePaginationInputs(\n variant,\n pageLabelFormatter,\n totalPages,\n currentPage,\n );\n const viewportMaxVisible = useViewportMaxVisible();\n\n const [pendingCurrentPage, setPendingCurrentPage] = useState<number | null>(\n null,\n );\n\n // Derive effective variant and page-slot count from viewport width.\n // Explicit 'grouped' is never auto-upgraded; only 'full' adapts.\n const adaptiveResult =\n resolvedVariant === 'full' ? viewportMaxVisible : MAX_VISIBLE_ELEMENTS;\n const effectiveVariant =\n resolvedVariant === 'full' && adaptiveResult === null\n ? 'grouped'\n : resolvedVariant;\n const maxVisible = adaptiveResult ?? MAX_VISIBLE_ELEMENTS;\n\n // Clamp current page to valid range\n const validCurrentPage = Math.max(\n 1,\n Math.min(sanitizedCurrentPage, sanitizedTotalPages),\n );\n\n const previousValidCurrentPageRef = useRef(validCurrentPage);\n\n // Keep the visual current page responsive while parent state catches up.\n // Once a controlled currentPage update arrives, return to controlled rendering.\n useEffect(() => {\n const didControlledPageChange =\n previousValidCurrentPageRef.current !== validCurrentPage;\n\n if (\n pendingCurrentPage !== null &&\n (pendingCurrentPage === validCurrentPage || didControlledPageChange)\n ) {\n setPendingCurrentPage(null);\n }\n\n previousValidCurrentPageRef.current = validCurrentPage;\n }, [validCurrentPage, pendingCurrentPage]);\n\n const visualCurrentPage = pendingCurrentPage ?? validCurrentPage;\n\n const canGoPrevious = visualCurrentPage > 1;\n const canGoNext = visualCurrentPage < sanitizedTotalPages;\n\n const {\n showBoundaryPages,\n pageNumbers,\n showLeftEllipsis,\n showRightEllipsis,\n } = useMemo(\n () =>\n calculateVisiblePageNumbers(\n visualCurrentPage,\n sanitizedTotalPages,\n maxVisible,\n ),\n [visualCurrentPage, sanitizedTotalPages, maxVisible],\n );\n\n // Only show pagination if there are 2 or more pages\n if (sanitizedTotalPages < 2) {\n return null;\n }\n\n const handlePageChange = (page: number) => {\n if (\n page !== visualCurrentPage &&\n page >= 1 &&\n page <= sanitizedTotalPages\n ) {\n setPendingCurrentPage(page);\n onPageChange(page);\n }\n };\n\n const classes = ['mds-pagination', `mds-pagination--${effectiveVariant}`];\n if (className) {\n classes.push(className);\n }\n\n // First, middle, and last page buttons share identical behavior; keeping the\n // markup here avoids three nearly identical JSX branches below.\n const renderPageButton = (page: number) => (\n <button\n key={page}\n type=\"button\"\n className=\"mds-pagination__page\"\n onClick={() => handlePageChange(page)}\n disabled={disabled}\n aria-label={resolvedPageLabelFormatter(page)}\n aria-current={page === visualCurrentPage ? 'page' : undefined}\n data-current={page === visualCurrentPage}\n >\n {page}\n </button>\n );\n\n return (\n <nav className={classes.join(' ')} aria-label={ariaLabel} {...props}>\n {/* Previous button */}\n <button\n type=\"button\"\n className=\"mds-pagination__button mds-pagination__button--prev\"\n onClick={() => handlePageChange(visualCurrentPage - 1)}\n disabled={disabled || !canGoPrevious}\n aria-label={previousPageLabel}\n tabIndex={!disabled && canGoPrevious ? 0 : -1}\n >\n <i className=\"fa-solid fa-chevron-left\" aria-hidden=\"true\" />\n </button>\n\n {/* Page numbers are hidden in the grouped variant. */}\n {effectiveVariant === 'full' && (\n <div className=\"mds-pagination__pages\">\n {showBoundaryPages && renderPageButton(1)}\n\n {/* Left ellipsis */}\n {showLeftEllipsis && (\n <span\n key=\"left-ellipsis\"\n className=\"mds-pagination__ellipsis\"\n aria-hidden=\"true\"\n >\n …\n </span>\n )}\n\n {/* Page numbers */}\n {pageNumbers.map(renderPageButton)}\n\n {/* Right ellipsis */}\n {showRightEllipsis && (\n <span\n key=\"right-ellipsis\"\n className=\"mds-pagination__ellipsis\"\n aria-hidden=\"true\"\n >\n …\n </span>\n )}\n\n {showBoundaryPages && renderPageButton(sanitizedTotalPages)}\n </div>\n )}\n\n {/* Next button */}\n <button\n type=\"button\"\n className=\"mds-pagination__button mds-pagination__button--next\"\n onClick={() => handlePageChange(visualCurrentPage + 1)}\n disabled={disabled || !canGoNext}\n aria-label={nextPageLabel}\n tabIndex={!disabled && canGoNext ? 0 : -1}\n >\n <i className=\"fa-solid fa-chevron-right\" aria-hidden=\"true\" />\n </button>\n </nav>\n );\n};\n"],"mappings":";;;;AASA,IAAM,uBAAuB;AAwC7B,IAAa,cAAc,EACzB,YACA,aACA,cACA,YAAY,cACZ,oBAAoB,iBACpB,gBAAgB,aAChB,oBACA,UAAU,QACV,WAAW,OACX,WACA,GAAG,YACkB;CACrB,MAAM,EACJ,iBACA,4BACA,qBACA,yBACE,wBACF,SACA,oBACA,YACA,WACF;CACA,MAAM,qBAAqB,sBAAsB;CAEjD,MAAM,CAAC,oBAAoB,yBAAyB,SAClD,IACF;CAIA,MAAM,iBACJ,oBAAoB,SAAS,qBAAqB;CACpD,MAAM,mBACJ,oBAAoB,UAAU,mBAAmB,OAC7C,YACA;CACN,MAAM,aAAa,kBAAkB;CAGrC,MAAM,mBAAmB,KAAK,IAC5B,GACA,KAAK,IAAI,sBAAsB,mBAAmB,CACpD;CAEA,MAAM,8BAA8B,OAAO,gBAAgB;CAI3D,gBAAgB;EACd,MAAM,0BACJ,4BAA4B,YAAY;EAE1C,IACE,uBAAuB,SACtB,uBAAuB,oBAAoB,0BAE5C,sBAAsB,IAAI;EAG5B,4BAA4B,UAAU;CACxC,GAAG,CAAC,kBAAkB,kBAAkB,CAAC;CAEzC,MAAM,oBAAoB,sBAAsB;CAEhD,MAAM,gBAAgB,oBAAoB;CAC1C,MAAM,YAAY,oBAAoB;CAEtC,MAAM,EACJ,mBACA,aACA,kBACA,sBACE,cAEA,4BACE,mBACA,qBACA,UACF,GACF;EAAC;EAAmB;EAAqB;CAAU,CACrD;CAGA,IAAI,sBAAsB,GACxB,OAAO;CAGT,MAAM,oBAAoB,SAAiB;EACzC,IACE,SAAS,qBACT,QAAQ,KACR,QAAQ,qBACR;GACA,sBAAsB,IAAI;GAC1B,aAAa,IAAI;EACnB;CACF;CAEA,MAAM,UAAU,CAAC,kBAAkB,mBAAmB,kBAAkB;CACxE,IAAI,WACF,QAAQ,KAAK,SAAS;CAKxB,MAAM,oBAAoB,SACxB,oBAAC,UAAD;EAEE,MAAK;EACL,WAAU;EACV,eAAe,iBAAiB,IAAI;EAC1B;EACV,cAAY,2BAA2B,IAAI;EAC3C,gBAAc,SAAS,oBAAoB,SAAS,KAAA;EACpD,gBAAc,SAAS;YAEtB;CACK,GAVD,IAUC;CAGV,OACE,qBAAC,OAAD;EAAK,WAAW,QAAQ,KAAK,GAAG;EAAG,cAAY;EAAW,GAAI;YAA9D;GAEE,oBAAC,UAAD;IACE,MAAK;IACL,WAAU;IACV,eAAe,iBAAiB,oBAAoB,CAAC;IACrD,UAAU,YAAY,CAAC;IACvB,cAAY;IACZ,UAAU,CAAC,YAAY,gBAAgB,IAAI;cAE3C,oBAAC,KAAD;KAAG,WAAU;KAA2B,eAAY;IAAQ,CAAA;GACtD,CAAA;GAGP,qBAAqB,UACpB,qBAAC,OAAD;IAAK,WAAU;cAAf;KACG,qBAAqB,iBAAiB,CAAC;KAGvC,oBACC,oBAAC,QAAD;MAEE,WAAU;MACV,eAAY;gBACb;KAEK,GALA,eAKA;KAIP,YAAY,IAAI,gBAAgB;KAGhC,qBACC,oBAAC,QAAD;MAEE,WAAU;MACV,eAAY;gBACb;KAEK,GALA,gBAKA;KAGP,qBAAqB,iBAAiB,mBAAmB;IACvD;;GAIP,oBAAC,UAAD;IACE,MAAK;IACL,WAAU;IACV,eAAe,iBAAiB,oBAAoB,CAAC;IACrD,UAAU,YAAY,CAAC;IACvB,cAAY;IACZ,UAAU,CAAC,YAAY,YAAY,IAAI;cAEvC,oBAAC,KAAD;KAAG,WAAU;KAA4B,eAAY;IAAQ,CAAA;GACvD,CAAA;EACL;;AAET"}
@@ -0,0 +1,139 @@
1
+ .mds-pagination {
2
+ display: flex;
3
+ gap: var(--mds-spacing-xxs);
4
+ align-items: center;
5
+ flex-wrap: wrap;
6
+ font-family: inherit;
7
+ direction: inherit;
8
+ }
9
+
10
+ /* Shared base styles for buttons and page items */
11
+ .mds-pagination__button,
12
+ .mds-pagination__page {
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ min-width: 34px;
17
+ min-height: 34px;
18
+ padding: var(--mds-spacing-xs);
19
+ border: var(--mds-stroke-weight-sm) solid transparent;
20
+ border-radius: var(--mds-border-radius-xs);
21
+ background-color: transparent;
22
+ color: var(--mds-text-default);
23
+ font-size: inherit;
24
+ font-family: inherit;
25
+ cursor: pointer;
26
+ -webkit-user-select: none;
27
+ user-select: none;
28
+ }
29
+
30
+ /* Keep hover affordance on prev/next controls only. Page-number buttons must
31
+ switch state atomically to avoid visible handoff when ellipsis positions move. */
32
+ .mds-pagination__button {
33
+ transition: background-color 150ms ease;
34
+ }
35
+
36
+ .mds-pagination__page {
37
+ transition: none;
38
+ }
39
+
40
+ .mds-pagination__button:hover:not(:disabled),
41
+ .mds-pagination__page:hover:not(:disabled) {
42
+ background-color: var(--mds-bg-interactive-secondary-hover);
43
+ }
44
+
45
+ .mds-pagination__button:active:not(:disabled),
46
+ .mds-pagination__page:active:not(:disabled) {
47
+ background-color: var(--mds-bg-interactive-secondary-active);
48
+ }
49
+
50
+ .mds-pagination__button:focus,
51
+ .mds-pagination__page:focus {
52
+ box-shadow: none;
53
+ outline: none;
54
+ }
55
+
56
+ .mds-pagination__button:focus-visible:not(:disabled),
57
+ .mds-pagination__page:focus-visible:not(:disabled) {
58
+ outline: var(--mds-stroke-weight-md) solid var(--mds-focus-default);
59
+ outline-offset: var(--mds-spacing-offset);
60
+ box-shadow: none;
61
+ }
62
+
63
+ .mds-pagination__button:disabled,
64
+ .mds-pagination__page:disabled {
65
+ color: var(--mds-text-muted);
66
+ cursor: not-allowed;
67
+ }
68
+
69
+ /* Icon sizing */
70
+ .mds-pagination__button i {
71
+ /* width/font-size use --mds-icons-sm (= scale-600 = 1rem = 16px, the icon md size). */
72
+ width: var(--mds-icons-sm);
73
+ font-size: var(--mds-icons-sm);
74
+ line-height: var(--mds-line-height-paragraph-xs);
75
+ }
76
+
77
+ /* Full variant */
78
+ .mds-pagination--full .mds-pagination__pages {
79
+ display: flex;
80
+ gap: var(--mds-spacing-xxs);
81
+ align-items: center;
82
+ }
83
+
84
+ /* Page items */
85
+ .mds-pagination__page {
86
+ line-height: var(--mds-line-height-paragraph-xs);
87
+ }
88
+
89
+ /* Current page state */
90
+ .mds-pagination__page[data-current='true'] {
91
+ background-color: var(--mds-bg-interactive-primary-default);
92
+ color: var(--mds-text-inverse);
93
+ }
94
+
95
+ .mds-pagination__page[data-current='true']:disabled {
96
+ background-color: transparent;
97
+ color: var(--mds-text-muted);
98
+ }
99
+
100
+ .mds-pagination__page[data-current='true']:hover:not(:disabled) {
101
+ background-color: var(--mds-bg-interactive-primary-hover);
102
+ }
103
+
104
+ .mds-pagination__page[data-current='true']:active:not(:disabled) {
105
+ background-color: var(--mds-bg-interactive-primary-active);
106
+ }
107
+
108
+ .mds-pagination__page[data-current='true']:focus-visible {
109
+ outline: var(--mds-stroke-weight-md) solid var(--mds-focus-default);
110
+ outline-offset: var(--mds-spacing-offset);
111
+ box-shadow: none;
112
+ }
113
+
114
+ /* Ellipsis */
115
+ .mds-pagination__ellipsis {
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ min-width: 34px;
120
+ min-height: 34px;
121
+ color: var(--mds-text-muted);
122
+ user-select: none;
123
+ pointer-events: none;
124
+ }
125
+
126
+ /* Grouped variant - only shows prev/next buttons */
127
+ .mds-pagination--grouped .mds-pagination__pages {
128
+ display: none;
129
+ }
130
+
131
+ /* Grouped button styling */
132
+ .mds-pagination--grouped .mds-pagination__button {
133
+ border-color: var(--mds-border-interactive-secondary-default);
134
+ }
135
+
136
+ /* Flip prev/next chevrons in RTL so they point in the correct travel direction */
137
+ [dir='rtl'] .mds-pagination__button i {
138
+ transform: scaleX(-1);
139
+ }
@@ -0,0 +1 @@
1
+ export * from './Pagination';
@@ -0,0 +1,2 @@
1
+ import { Pagination } from "./Pagination.js";
2
+ export { Pagination };
@@ -0,0 +1,26 @@
1
+ export type PaginationVariant = 'full' | 'grouped';
2
+ export type PageLabelFormatter = (page: number) => string;
3
+ interface ResolvedPaginationInputs {
4
+ resolvedVariant: PaginationVariant;
5
+ resolvedPageLabelFormatter: PageLabelFormatter;
6
+ sanitizedTotalPages: number;
7
+ sanitizedCurrentPage: number;
8
+ }
9
+ export declare const MAX_VISIBLE_ELEMENTS = 9;
10
+ /**
11
+ * Calculate which page numbers to display given the current page, total pages,
12
+ * and the maximum number of visible slots (boundaries + ellipses + center pages).
13
+ *
14
+ * Slot accounting for any maxVisible M:
15
+ * Near start / near end (1 ellipsis): 1 boundary + M-3 center + 1 ellipsis + 1 boundary
16
+ * Middle (2 ellipses): 1 boundary + 1 ellipsis + M-4 center + 1 ellipsis + 1 boundary
17
+ */
18
+ export declare function calculateVisiblePageNumbers(currentPage: number, totalPages: number, maxVisible: number): {
19
+ showBoundaryPages: boolean;
20
+ showLeftEllipsis: boolean;
21
+ showRightEllipsis: boolean;
22
+ pageNumbers: number[];
23
+ };
24
+ export declare function useViewportMaxVisible(): number | null;
25
+ export declare function resolvePaginationInputs(variant: string, pageLabelFormatter: PageLabelFormatter | undefined, totalPages: number, currentPage: number): ResolvedPaginationInputs;
26
+ export {};
@@ -0,0 +1,136 @@
1
+ import { useEffect, useState } from "react";
2
+ var allowedVariants = ["full", "grouped"];
3
+ var VIEWPORT_BREAKPOINT_MEDIA_QUERIES = [
4
+ "(min-width: 576px)",
5
+ "(min-width: 768px)",
6
+ "(min-width: 992px)"
7
+ ];
8
+ var defaultPageLabelFormatter = (page) => `Page ${page}`;
9
+ /**
10
+ * Returns how many page-number slots to show based on the current viewport width.
11
+ * Returns null when the viewport is too narrow for even a minimal 5-item row,
12
+ * which signals an auto-collapse to grouped appearance.
13
+ *
14
+ * Thresholds align with MDS / Bootstrap 5 breakpoints:
15
+ * xxs < 576 px → null (grouped, no page numbers)
16
+ * sm ≥ 576 px → 5
17
+ * md ≥ 768 px → 7
18
+ * lg ≥ 992 px → 9
19
+ */
20
+ function getViewportMaxVisible() {
21
+ if (typeof window === "undefined") return 9;
22
+ const w = window.innerWidth;
23
+ if (w >= 992) return 9;
24
+ if (w >= 768) return 7;
25
+ if (w >= 576) return 5;
26
+ return null;
27
+ }
28
+ /**
29
+ * Calculate which page numbers to display given the current page, total pages,
30
+ * and the maximum number of visible slots (boundaries + ellipses + center pages).
31
+ *
32
+ * Slot accounting for any maxVisible M:
33
+ * Near start / near end (1 ellipsis): 1 boundary + M-3 center + 1 ellipsis + 1 boundary
34
+ * Middle (2 ellipses): 1 boundary + 1 ellipsis + M-4 center + 1 ellipsis + 1 boundary
35
+ */
36
+ function calculateVisiblePageNumbers(currentPage, totalPages, maxVisible) {
37
+ if (totalPages <= maxVisible) return {
38
+ showBoundaryPages: false,
39
+ showLeftEllipsis: false,
40
+ showRightEllipsis: false,
41
+ pageNumbers: Array.from({ length: totalPages }, (_, i) => i + 1)
42
+ };
43
+ const middleForSingle = maxVisible - 3;
44
+ const middleForDouble = maxVisible - 4;
45
+ const halfMiddle = Math.floor(middleForDouble / 2);
46
+ const nearStartThreshold = 1 + middleForSingle - halfMiddle;
47
+ const nearEndThreshold = totalPages - nearStartThreshold + 1;
48
+ if (currentPage <= nearStartThreshold) {
49
+ const startPage = 2;
50
+ const endPage = 1 + middleForSingle;
51
+ return {
52
+ showBoundaryPages: true,
53
+ showLeftEllipsis: false,
54
+ showRightEllipsis: true,
55
+ pageNumbers: Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i)
56
+ };
57
+ }
58
+ if (currentPage >= nearEndThreshold) {
59
+ const endPage = totalPages - 1;
60
+ const startPage = endPage - middleForSingle + 1;
61
+ return {
62
+ showBoundaryPages: true,
63
+ showLeftEllipsis: true,
64
+ showRightEllipsis: false,
65
+ pageNumbers: Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i)
66
+ };
67
+ }
68
+ const startPage = currentPage - halfMiddle;
69
+ const endPage = currentPage + middleForDouble - halfMiddle - 1;
70
+ return {
71
+ showBoundaryPages: true,
72
+ showLeftEllipsis: true,
73
+ showRightEllipsis: true,
74
+ pageNumbers: Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i)
75
+ };
76
+ }
77
+ function sanitizePositiveInteger(value, propName, fallbackValue) {
78
+ if (!Number.isFinite(value)) return fallbackValue;
79
+ const normalizedValue = Math.trunc(value);
80
+ if (normalizedValue < 1 || normalizedValue !== value) return Math.max(1, normalizedValue);
81
+ return normalizedValue;
82
+ }
83
+ function addMediaQueryChangeListener(mediaQueryList, listener) {
84
+ if (typeof mediaQueryList.addEventListener === "function") {
85
+ mediaQueryList.addEventListener("change", listener);
86
+ return;
87
+ }
88
+ mediaQueryList.addListener(listener);
89
+ }
90
+ function removeMediaQueryChangeListener(mediaQueryList, listener) {
91
+ if (typeof mediaQueryList.removeEventListener === "function") {
92
+ mediaQueryList.removeEventListener("change", listener);
93
+ return;
94
+ }
95
+ mediaQueryList.removeListener(listener);
96
+ }
97
+ function useViewportMaxVisible() {
98
+ const [viewportMaxVisible, setViewportMaxVisible] = useState(getViewportMaxVisible);
99
+ useEffect(() => {
100
+ if (typeof window === "undefined") return;
101
+ const handler = () => setViewportMaxVisible(getViewportMaxVisible());
102
+ if (typeof window.matchMedia !== "function") {
103
+ window.addEventListener("resize", handler);
104
+ return () => window.removeEventListener("resize", handler);
105
+ }
106
+ const mediaQueryLists = VIEWPORT_BREAKPOINT_MEDIA_QUERIES.map((query) => window.matchMedia(query));
107
+ mediaQueryLists.forEach((mediaQueryList) => {
108
+ addMediaQueryChangeListener(mediaQueryList, handler);
109
+ });
110
+ return () => {
111
+ mediaQueryLists.forEach((mediaQueryList) => {
112
+ removeMediaQueryChangeListener(mediaQueryList, handler);
113
+ });
114
+ };
115
+ }, []);
116
+ return viewportMaxVisible;
117
+ }
118
+ function resolvePaginationVariant(variant) {
119
+ return allowedVariants.includes(variant) ? variant : "full";
120
+ }
121
+ function resolvePageLabelFormatter(pageLabelFormatter) {
122
+ if (pageLabelFormatter !== void 0 && typeof pageLabelFormatter !== "function");
123
+ return typeof pageLabelFormatter === "function" ? pageLabelFormatter : defaultPageLabelFormatter;
124
+ }
125
+ function resolvePaginationInputs(variant, pageLabelFormatter, totalPages, currentPage) {
126
+ return {
127
+ resolvedVariant: resolvePaginationVariant(variant),
128
+ resolvedPageLabelFormatter: resolvePageLabelFormatter(pageLabelFormatter),
129
+ sanitizedTotalPages: sanitizePositiveInteger(totalPages, "totalPages", 1),
130
+ sanitizedCurrentPage: sanitizePositiveInteger(currentPage, "currentPage", 1)
131
+ };
132
+ }
133
+ //#endregion
134
+ export { calculateVisiblePageNumbers, resolvePaginationInputs, useViewportMaxVisible };
135
+
136
+ //# sourceMappingURL=pagination.helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination.helpers.js","names":[],"sources":["../../../components/pagination/pagination.helpers.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\nexport type PaginationVariant = 'full' | 'grouped';\nexport type PageLabelFormatter = (page: number) => string;\n\ninterface ResolvedPaginationInputs {\n resolvedVariant: PaginationVariant;\n resolvedPageLabelFormatter: PageLabelFormatter;\n sanitizedTotalPages: number;\n sanitizedCurrentPage: number;\n}\n\nexport const MAX_VISIBLE_ELEMENTS = 9;\n\nconst allowedVariants: PaginationVariant[] = ['full', 'grouped'];\nconst VIEWPORT_BREAKPOINT_MEDIA_QUERIES = [\n '(min-width: 576px)',\n '(min-width: 768px)',\n '(min-width: 992px)',\n] as const;\nconst defaultPageLabelFormatter: PageLabelFormatter = (page) => `Page ${page}`;\n\n/**\n * Returns how many page-number slots to show based on the current viewport width.\n * Returns null when the viewport is too narrow for even a minimal 5-item row,\n * which signals an auto-collapse to grouped appearance.\n *\n * Thresholds align with MDS / Bootstrap 5 breakpoints:\n * xxs < 576 px → null (grouped, no page numbers)\n * sm ≥ 576 px → 5\n * md ≥ 768 px → 7\n * lg ≥ 992 px → 9\n */\nfunction getViewportMaxVisible(): number | null {\n if (typeof window === 'undefined') return MAX_VISIBLE_ELEMENTS;\n const w = window.innerWidth;\n if (w >= 992) return 9;\n if (w >= 768) return 7;\n if (w >= 576) return 5;\n return null;\n}\n\n/**\n * Calculate which page numbers to display given the current page, total pages,\n * and the maximum number of visible slots (boundaries + ellipses + center pages).\n *\n * Slot accounting for any maxVisible M:\n * Near start / near end (1 ellipsis): 1 boundary + M-3 center + 1 ellipsis + 1 boundary\n * Middle (2 ellipses): 1 boundary + 1 ellipsis + M-4 center + 1 ellipsis + 1 boundary\n */\nexport function calculateVisiblePageNumbers(\n currentPage: number,\n totalPages: number,\n maxVisible: number,\n): {\n showBoundaryPages: boolean;\n showLeftEllipsis: boolean;\n showRightEllipsis: boolean;\n pageNumbers: number[];\n} {\n // All pages fit within budget — no boundary markers or ellipses needed.\n if (totalPages <= maxVisible) {\n return {\n showBoundaryPages: false,\n showLeftEllipsis: false,\n showRightEllipsis: false,\n pageNumbers: Array.from({ length: totalPages }, (_, i) => i + 1),\n };\n }\n\n // Center pages when a single ellipsis is visible (near start or near end).\n const middleForSingle = maxVisible - 3;\n // Center pages when both ellipses are visible (middle position).\n const middleForDouble = maxVisible - 4;\n const halfMiddle = Math.floor(middleForDouble / 2);\n\n // Page at which the left ellipsis first appears (near-start → middle transition).\n const nearStartThreshold = 1 + middleForSingle - halfMiddle;\n // Page at which the right ellipsis disappears (middle → near-end transition).\n const nearEndThreshold = totalPages - nearStartThreshold + 1;\n\n if (currentPage <= nearStartThreshold) {\n const startPage = 2;\n const endPage = 1 + middleForSingle;\n return {\n showBoundaryPages: true,\n showLeftEllipsis: false,\n showRightEllipsis: true,\n pageNumbers: Array.from(\n { length: endPage - startPage + 1 },\n (_, i) => startPage + i,\n ),\n };\n }\n\n if (currentPage >= nearEndThreshold) {\n const endPage = totalPages - 1;\n const startPage = endPage - middleForSingle + 1;\n return {\n showBoundaryPages: true,\n showLeftEllipsis: true,\n showRightEllipsis: false,\n pageNumbers: Array.from(\n { length: endPage - startPage + 1 },\n (_, i) => startPage + i,\n ),\n };\n }\n\n // Middle position — both ellipses visible.\n const startPage = currentPage - halfMiddle;\n const endPage = currentPage + middleForDouble - halfMiddle - 1;\n return {\n showBoundaryPages: true,\n showLeftEllipsis: true,\n showRightEllipsis: true,\n pageNumbers: Array.from(\n { length: endPage - startPage + 1 },\n (_, i) => startPage + i,\n ),\n };\n}\n\nfunction warnInvalidProp(\n propName: string,\n value: unknown,\n fallbackDescription: string,\n) {\n if (!import.meta.env.DEV) {\n return;\n }\n\n console.warn(\n `[MDS Pagination] Invalid ${propName} \"${String(value)}\". Falling back to ${fallbackDescription}.`,\n );\n}\n\nfunction sanitizePositiveInteger(\n value: number,\n propName: 'totalPages' | 'currentPage',\n fallbackValue: number,\n): number {\n if (!Number.isFinite(value)) {\n warnInvalidProp(propName, value, String(fallbackValue));\n return fallbackValue;\n }\n\n const normalizedValue = Math.trunc(value);\n if (normalizedValue < 1 || normalizedValue !== value) {\n const safeValue = Math.max(1, normalizedValue);\n warnInvalidProp(propName, value, String(safeValue));\n return safeValue;\n }\n\n return normalizedValue;\n}\n\nfunction addMediaQueryChangeListener(\n mediaQueryList: MediaQueryList,\n listener: () => void,\n) {\n // Safari still supports the older addListener/removeListener pair, so keep\n // this compatibility layer until the library drops that browser range.\n if (typeof mediaQueryList.addEventListener === 'function') {\n mediaQueryList.addEventListener('change', listener);\n return;\n }\n\n mediaQueryList.addListener(listener);\n}\n\nfunction removeMediaQueryChangeListener(\n mediaQueryList: MediaQueryList,\n listener: () => void,\n) {\n if (typeof mediaQueryList.removeEventListener === 'function') {\n mediaQueryList.removeEventListener('change', listener);\n return;\n }\n\n mediaQueryList.removeListener(listener);\n}\n\nexport function useViewportMaxVisible() {\n // Initialise from current viewport width so the first render already matches the viewport.\n const [viewportMaxVisible, setViewportMaxVisible] = useState<number | null>(\n getViewportMaxVisible,\n );\n\n useEffect(() => {\n // window is available in browser environments; absent in jsdom / SSR.\n if (typeof window === 'undefined') return;\n\n const handler = () => setViewportMaxVisible(getViewportMaxVisible());\n\n if (typeof window.matchMedia !== 'function') {\n window.addEventListener('resize', handler);\n return () => window.removeEventListener('resize', handler);\n }\n\n const mediaQueryLists = VIEWPORT_BREAKPOINT_MEDIA_QUERIES.map((query) =>\n window.matchMedia(query),\n );\n\n mediaQueryLists.forEach((mediaQueryList) => {\n addMediaQueryChangeListener(mediaQueryList, handler);\n });\n\n return () => {\n mediaQueryLists.forEach((mediaQueryList) => {\n removeMediaQueryChangeListener(mediaQueryList, handler);\n });\n };\n }, []);\n\n return viewportMaxVisible;\n}\n\nfunction resolvePaginationVariant(variant: string): PaginationVariant {\n if (\n import.meta.env.DEV &&\n variant &&\n !allowedVariants.includes(variant as PaginationVariant)\n ) {\n console.warn(\n `[MDS Pagination] Invalid variant \"${variant}\". Falling back to \"full\". Allowed: ${allowedVariants.join(', ')}`,\n );\n }\n\n return allowedVariants.includes(variant as PaginationVariant)\n ? (variant as PaginationVariant)\n : 'full';\n}\n\nfunction resolvePageLabelFormatter(pageLabelFormatter?: PageLabelFormatter) {\n if (\n pageLabelFormatter !== undefined &&\n typeof pageLabelFormatter !== 'function'\n ) {\n warnInvalidProp(\n 'pageLabelFormatter',\n pageLabelFormatter,\n 'the default page label formatter',\n );\n }\n\n return typeof pageLabelFormatter === 'function'\n ? pageLabelFormatter\n : defaultPageLabelFormatter;\n}\n\nexport function resolvePaginationInputs(\n variant: string,\n pageLabelFormatter: PageLabelFormatter | undefined,\n totalPages: number,\n currentPage: number,\n): ResolvedPaginationInputs {\n // Resolve every JS-facing input in one place so the render path can assume\n // normalized values and stay focused on layout decisions.\n return {\n resolvedVariant: resolvePaginationVariant(variant),\n resolvedPageLabelFormatter: resolvePageLabelFormatter(pageLabelFormatter),\n sanitizedTotalPages: sanitizePositiveInteger(totalPages, 'totalPages', 1),\n sanitizedCurrentPage: sanitizePositiveInteger(\n currentPage,\n 'currentPage',\n 1,\n ),\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import { HTMLAttributes } from 'react';
2
+ type ProgressBarStatus = 'in-progress' | 'loading' | 'error' | 'warning';
3
+ type ProgressBarLabelVariant = 'title-and-count' | 'title' | 'inline' | 'none';
4
+ export interface ProgressBarProps extends HTMLAttributes<HTMLDivElement> {
5
+ /** Current progress value in the range defined by `min` and `max`. */
6
+ value?: number;
7
+ /** Lower bound for the progress range. Defaults to 0. */
8
+ min?: number;
9
+ /** Upper bound for the progress range. Defaults to 100. */
10
+ max?: number;
11
+ /** Visual state of the progress indicator — controls fill and track colours. */
12
+ status?: ProgressBarStatus;
13
+ /**
14
+ * Controls label layout:
15
+ * - `title-and-count`: title + count row above the bar (default)
16
+ * - `title`: title only above the bar
17
+ * - `inline`: bar with count beside it in a row
18
+ * - `none`: bar only, no visible label
19
+ */
20
+ labelVariant?: ProgressBarLabelVariant;
21
+ /** Pre-translated title text rendered above the bar (title-and-count / title variants).
22
+ * Also used as the accessible name for the bar when no visible label is present. */
23
+ title?: string;
24
+ /**
25
+ * Pre-translated count or percentage text, e.g. "3 of 10" or "50%".
26
+ * The component intentionally does not calculate or format this from
27
+ * `value`, `min`, and `max`; word order, plural rules, and number formatting
28
+ * belong to the consuming application's i18n layer.
29
+ */
30
+ count?: string;
31
+ /** Controls striped animation when status is loading. */
32
+ animated?: boolean;
33
+ }
34
+ export declare const ProgressBar: ({ value, min, max, status, labelVariant, title, count, animated, className, ...props }: ProgressBarProps) => import("react").JSX.Element;
35
+ export {};
@@ -0,0 +1,86 @@
1
+ import { useId } from "react";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region components/progress-bar/ProgressBar.tsx
4
+ var allowedStatuses = [
5
+ "in-progress",
6
+ "loading",
7
+ "error",
8
+ "warning"
9
+ ];
10
+ var allowedLabels = [
11
+ "title-and-count",
12
+ "title",
13
+ "inline",
14
+ "none"
15
+ ];
16
+ var ProgressBar = ({ value = 0, min = 0, max = 100, status, labelVariant = "title-and-count", title, count, animated = false, className, ...props }) => {
17
+ const titleId = useId();
18
+ const isAllowedStatus = !!status && allowedStatuses.includes(status);
19
+ const isAllowedLabelVariant = allowedLabels.includes(labelVariant);
20
+ const hasAscendingRange = Number.isFinite(min) && Number.isFinite(max) && min < max;
21
+ const resolvedMin = hasAscendingRange ? min : 0;
22
+ const resolvedMax = hasAscendingRange ? max : 100;
23
+ const resolvedStatus = isAllowedStatus ? status : "in-progress";
24
+ const resolvedLabel = isAllowedLabelVariant ? labelVariant : "title-and-count";
25
+ const clampedValue = Math.min(resolvedMax, Math.max(resolvedMin, value));
26
+ const fillPercent = (clampedValue - resolvedMin) / (resolvedMax - resolvedMin) * 100;
27
+ const visualStatus = fillPercent === 0 ? "empty" : fillPercent === 100 ? "completed" : resolvedStatus;
28
+ const isTitleCount = resolvedLabel === "title-and-count";
29
+ const isTitle = resolvedLabel === "title";
30
+ const isInline = resolvedLabel === "inline";
31
+ const showTitleRow = isTitleCount || isTitle;
32
+ const { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, ...restProps } = props;
33
+ const hasVisibleTitle = showTitleRow && Boolean(title);
34
+ const trackLabelledby = ariaLabelledby ?? (hasVisibleTitle && !ariaLabel ? titleId : void 0);
35
+ const trackAriaLabel = ariaLabel ?? (!hasVisibleTitle ? title : void 0);
36
+ const wrapperClasses = [
37
+ "mds-progress-bar",
38
+ `mds-progress-bar--label-${resolvedLabel}`,
39
+ `mds-progress-bar--${visualStatus}`
40
+ ];
41
+ if (className) wrapperClasses.push(className);
42
+ const fillClasses = [
43
+ "mds-progress-bar-fill",
44
+ "progress-bar",
45
+ visualStatus === "loading" ? "progress-bar-striped" : "",
46
+ visualStatus === "loading" && animated ? "progress-bar-animated" : ""
47
+ ].filter(Boolean).join(" ");
48
+ return /* @__PURE__ */ jsxs("div", {
49
+ className: wrapperClasses.join(" "),
50
+ ...restProps,
51
+ children: [
52
+ showTitleRow && /* @__PURE__ */ jsxs("div", {
53
+ className: "mds-progress-bar-label",
54
+ children: [/* @__PURE__ */ jsx("span", {
55
+ id: titleId,
56
+ className: "mds-progress-bar-title",
57
+ children: title
58
+ }), isTitleCount && /* @__PURE__ */ jsx("span", {
59
+ className: "mds-progress-bar-count",
60
+ children: count
61
+ })]
62
+ }),
63
+ /* @__PURE__ */ jsx("div", {
64
+ className: "mds-progress-bar-track progress",
65
+ role: "progressbar",
66
+ "aria-valuenow": clampedValue,
67
+ "aria-valuemin": resolvedMin,
68
+ "aria-valuemax": resolvedMax,
69
+ "aria-labelledby": trackLabelledby,
70
+ "aria-label": trackAriaLabel,
71
+ children: /* @__PURE__ */ jsx("div", {
72
+ className: fillClasses,
73
+ style: { width: `${fillPercent}%` }
74
+ })
75
+ }),
76
+ isInline && /* @__PURE__ */ jsx("span", {
77
+ className: "mds-progress-bar-count",
78
+ children: count
79
+ })
80
+ ]
81
+ });
82
+ };
83
+ //#endregion
84
+ export { ProgressBar };
85
+
86
+ //# sourceMappingURL=ProgressBar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProgressBar.js","names":[],"sources":["../../../components/progress-bar/ProgressBar.tsx"],"sourcesContent":["import type { HTMLAttributes } from 'react';\nimport { useId } from 'react';\n\ntype ProgressBarStatus = 'in-progress' | 'loading' | 'error' | 'warning';\n\ntype ProgressBarVisualStatus = 'empty' | ProgressBarStatus | 'completed';\n\ntype ProgressBarLabelVariant = 'title-and-count' | 'title' | 'inline' | 'none';\n\nexport interface ProgressBarProps extends HTMLAttributes<HTMLDivElement> {\n /** Current progress value in the range defined by `min` and `max`. */\n value?: number;\n /** Lower bound for the progress range. Defaults to 0. */\n min?: number;\n /** Upper bound for the progress range. Defaults to 100. */\n max?: number;\n /** Visual state of the progress indicator — controls fill and track colours. */\n status?: ProgressBarStatus;\n /**\n * Controls label layout:\n * - `title-and-count`: title + count row above the bar (default)\n * - `title`: title only above the bar\n * - `inline`: bar with count beside it in a row\n * - `none`: bar only, no visible label\n */\n labelVariant?: ProgressBarLabelVariant;\n /** Pre-translated title text rendered above the bar (title-and-count / title variants).\n * Also used as the accessible name for the bar when no visible label is present. */\n title?: string;\n /**\n * Pre-translated count or percentage text, e.g. \"3 of 10\" or \"50%\".\n * The component intentionally does not calculate or format this from\n * `value`, `min`, and `max`; word order, plural rules, and number formatting\n * belong to the consuming application's i18n layer.\n */\n count?: string;\n /** Controls striped animation when status is loading. */\n animated?: boolean;\n}\n\nconst allowedStatuses: ProgressBarStatus[] = [\n 'in-progress',\n 'loading',\n 'error',\n 'warning',\n];\n\nconst allowedLabels: ProgressBarLabelVariant[] = [\n 'title-and-count',\n 'title',\n 'inline',\n 'none',\n];\n\nexport const ProgressBar = ({\n value = 0,\n min = 0,\n max = 100,\n status,\n labelVariant = 'title-and-count',\n title,\n count,\n animated = false,\n className,\n ...props\n}: ProgressBarProps) => {\n const titleId = useId();\n\n const isAllowedStatus =\n !!status && allowedStatuses.includes(status as ProgressBarStatus);\n const isAllowedLabelVariant = allowedLabels.includes(\n labelVariant as ProgressBarLabelVariant,\n );\n\n // Normalize range inputs to finite numbers and ensure an ascending range.\n const hasFiniteRange = Number.isFinite(min) && Number.isFinite(max);\n const hasAscendingRange = hasFiniteRange && min < max;\n const resolvedMin = hasAscendingRange ? min : 0;\n const resolvedMax = hasAscendingRange ? max : 100;\n const isValueInRange = value >= resolvedMin && value <= resolvedMax;\n\n const resolvedStatus: ProgressBarStatus = isAllowedStatus\n ? status\n : 'in-progress';\n const resolvedLabel: ProgressBarLabelVariant = isAllowedLabelVariant\n ? labelVariant\n : 'title-and-count';\n const clampedValue = Math.min(resolvedMax, Math.max(resolvedMin, value));\n const fillPercent =\n ((clampedValue - resolvedMin) / (resolvedMax - resolvedMin)) * 100;\n // At 0% and 100% fill, visual styling is derived from normalized position,\n // regardless of status.\n const visualStatus: ProgressBarVisualStatus =\n fillPercent === 0\n ? 'empty'\n : fillPercent === 100\n ? 'completed'\n : resolvedStatus;\n\n if (import.meta.env.DEV) {\n if (status && !isAllowedStatus) {\n console.warn(\n `[MDS ProgressBar] Invalid status \"${status}\". Falling back to \"in-progress\". Allowed: ${allowedStatuses.join(', ')}`,\n );\n }\n if (!isAllowedLabelVariant) {\n console.warn(\n `[MDS ProgressBar] Invalid labelVariant \"${labelVariant}\". Falling back to \"title-and-count\". Allowed: ${allowedLabels.join(', ')}`,\n );\n }\n if (!hasAscendingRange) {\n console.warn(\n `[MDS ProgressBar] Invalid range min=\"${min}\" max=\"${max}\". Falling back to min=0 and max=100 with min < max required.`,\n );\n }\n if (!isValueInRange) {\n console.warn(\n `[MDS ProgressBar] value \"${value}\" is outside the allowed range [${resolvedMin}–${resolvedMax}] and will be clamped.`,\n );\n }\n }\n\n const isTitleCount = resolvedLabel === 'title-and-count';\n const isTitle = resolvedLabel === 'title';\n const isInline = resolvedLabel === 'inline';\n const showTitleRow = isTitleCount || isTitle;\n\n // Extract aria-* props so they can be forwarded to the progressbar track element,\n // which carries role=\"progressbar\" — the accessible element in this component.\n const {\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n ...restProps\n } = props;\n\n // Derive accessible name for the track:\n // - Explicit aria-labelledby wins if provided.\n // - If a visible title element is present and no explicit aria-label, point to it.\n const hasVisibleTitle = showTitleRow && Boolean(title);\n const trackLabelledby =\n ariaLabelledby ?? (hasVisibleTitle && !ariaLabel ? titleId : undefined);\n\n // aria-label is used when no visible title is rendered (inline / none variants)\n // and no labelledby reference applies.\n const trackAriaLabel = ariaLabel ?? (!hasVisibleTitle ? title : undefined);\n\n // Check derived accessible-name values regardless of the labelVariant\n if (import.meta.env.DEV && !trackAriaLabel && !trackLabelledby) {\n console.warn(\n '[MDS ProgressBar] No accessible name found. Provide a title prop or aria-label / aria-labelledby.',\n );\n }\n\n const wrapperClasses = [\n 'mds-progress-bar',\n `mds-progress-bar--label-${resolvedLabel}`,\n `mds-progress-bar--${visualStatus}`,\n ];\n if (className) wrapperClasses.push(className);\n\n const fillClasses = [\n 'mds-progress-bar-fill',\n 'progress-bar',\n // In loading status, stripes are always shown; motion is controlled by `animated`.\n visualStatus === 'loading' ? 'progress-bar-striped' : '',\n visualStatus === 'loading' && animated ? 'progress-bar-animated' : '',\n ]\n .filter(Boolean)\n .join(' ');\n\n return (\n <div className={wrapperClasses.join(' ')} {...restProps}>\n {showTitleRow && (\n <div className=\"mds-progress-bar-label\">\n <span id={titleId} className=\"mds-progress-bar-title\">\n {title}\n </span>\n {isTitleCount && (\n <span className=\"mds-progress-bar-count\">{count}</span>\n )}\n </div>\n )}\n <div\n className=\"mds-progress-bar-track progress\"\n role=\"progressbar\"\n aria-valuenow={clampedValue}\n aria-valuemin={resolvedMin}\n aria-valuemax={resolvedMax}\n aria-labelledby={trackLabelledby}\n aria-label={trackAriaLabel}\n >\n <div className={fillClasses} style={{ width: `${fillPercent}%` }} />\n </div>\n {isInline && <span className=\"mds-progress-bar-count\">{count}</span>}\n </div>\n );\n};\n"],"mappings":";;;AAwCA,IAAM,kBAAuC;CAC3C;CACA;CACA;CACA;AACF;AAEA,IAAM,gBAA2C;CAC/C;CACA;CACA;CACA;AACF;AAEA,IAAa,eAAe,EAC1B,QAAQ,GACR,MAAM,GACN,MAAM,KACN,QACA,eAAe,mBACf,OACA,OACA,WAAW,OACX,WACA,GAAG,YACmB;CACtB,MAAM,UAAU,MAAM;CAEtB,MAAM,kBACJ,CAAC,CAAC,UAAU,gBAAgB,SAAS,MAA2B;CAClE,MAAM,wBAAwB,cAAc,SAC1C,YACF;CAIA,MAAM,oBADiB,OAAO,SAAS,GAAG,KAAK,OAAO,SAAS,GAAG,KACtB,MAAM;CAClD,MAAM,cAAc,oBAAoB,MAAM;CAC9C,MAAM,cAAc,oBAAoB,MAAM;CAG9C,MAAM,iBAAoC,kBACtC,SACA;CACJ,MAAM,gBAAyC,wBAC3C,eACA;CACJ,MAAM,eAAe,KAAK,IAAI,aAAa,KAAK,IAAI,aAAa,KAAK,CAAC;CACvE,MAAM,eACF,eAAe,gBAAgB,cAAc,eAAgB;CAGjE,MAAM,eACJ,gBAAgB,IACZ,UACA,gBAAgB,MACd,cACA;CAyBR,MAAM,eAAe,kBAAkB;CACvC,MAAM,UAAU,kBAAkB;CAClC,MAAM,WAAW,kBAAkB;CACnC,MAAM,eAAe,gBAAgB;CAIrC,MAAM,EACJ,cAAc,WACd,mBAAmB,gBACnB,GAAG,cACD;CAKJ,MAAM,kBAAkB,gBAAgB,QAAQ,KAAK;CACrD,MAAM,kBACJ,mBAAmB,mBAAmB,CAAC,YAAY,UAAU,KAAA;CAI/D,MAAM,iBAAiB,cAAc,CAAC,kBAAkB,QAAQ,KAAA;CAShE,MAAM,iBAAiB;EACrB;EACA,2BAA2B;EAC3B,qBAAqB;CACvB;CACA,IAAI,WAAW,eAAe,KAAK,SAAS;CAE5C,MAAM,cAAc;EAClB;EACA;EAEA,iBAAiB,YAAY,yBAAyB;EACtD,iBAAiB,aAAa,WAAW,0BAA0B;CACrE,EACG,OAAO,OAAO,EACd,KAAK,GAAG;CAEX,OACE,qBAAC,OAAD;EAAK,WAAW,eAAe,KAAK,GAAG;EAAG,GAAI;YAA9C;GACG,gBACC,qBAAC,OAAD;IAAK,WAAU;cAAf,CACE,oBAAC,QAAD;KAAM,IAAI;KAAS,WAAU;eAC1B;IACG,CAAA,GACL,gBACC,oBAAC,QAAD;KAAM,WAAU;eAA0B;IAAY,CAAA,CAErD;;GAEP,oBAAC,OAAD;IACE,WAAU;IACV,MAAK;IACL,iBAAe;IACf,iBAAe;IACf,iBAAe;IACf,mBAAiB;IACjB,cAAY;cAEZ,oBAAC,OAAD;KAAK,WAAW;KAAa,OAAO,EAAE,OAAO,GAAG,YAAY,GAAG;IAAI,CAAA;GAChE,CAAA;GACJ,YAAY,oBAAC,QAAD;IAAM,WAAU;cAA0B;GAAY,CAAA;EAChE;;AAET"}