@moodlehq/design-system 2.1.0 → 3.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 (40) hide show
  1. package/README.md +6 -1
  2. package/css.d.ts +5 -0
  3. package/dist/components/button/Button.d.ts +9 -7
  4. package/dist/components/button/Button.js +37 -0
  5. package/dist/components/button/Button.js.map +1 -0
  6. package/dist/components/button/index.js +2 -0
  7. package/dist/components/close-button/CloseButton.d.ts +14 -0
  8. package/dist/components/close-button/CloseButton.js +25 -0
  9. package/dist/components/close-button/CloseButton.js.map +1 -0
  10. package/dist/components/close-button/index.d.ts +1 -0
  11. package/dist/components/close-button/index.js +2 -0
  12. package/dist/components/index.d.ts +4 -4
  13. package/dist/components/radio/Radio.d.ts +22 -0
  14. package/dist/components/radio/Radio.js +47 -0
  15. package/dist/components/radio/Radio.js.map +1 -0
  16. package/dist/components/radio/index.d.ts +1 -0
  17. package/dist/components/radio/index.js +2 -0
  18. package/dist/index.css +362 -279
  19. package/dist/index.js +7 -3262
  20. package/package.json +31 -12
  21. package/tokens/css/borders.css +4 -3
  22. package/tokens/css/breakpoints.css +11 -0
  23. package/tokens/css/colors.css +20 -2
  24. package/tokens/css/index.css +1 -0
  25. package/tokens/css/shadows.css +3 -3
  26. package/tokens/css/sizes.css +7 -6
  27. package/tokens/css/spacing.css +1 -0
  28. package/tokens/scss/_borders.scss +4 -3
  29. package/tokens/scss/_breakpoints.scss +8 -0
  30. package/tokens/scss/_colors.scss +20 -2
  31. package/tokens/scss/_index.legacy.scss +1 -0
  32. package/tokens/scss/_index.scss +1 -0
  33. package/tokens/scss/_shadows.scss +3 -3
  34. package/tokens/scss/_sizes.scss +7 -6
  35. package/tokens/scss/_spacing.scss +1 -0
  36. package/dist/components/alert/Alert.d.ts +0 -14
  37. package/dist/components/alert/index.d.ts +0 -2
  38. package/dist/components/icon/Icon.d.ts +0 -7
  39. package/dist/components/icon/index.d.ts +0 -1
  40. package/dist/index.js.map +0 -1
package/README.md CHANGED
@@ -100,7 +100,12 @@ export default function App() {
100
100
  }
101
101
  ```
102
102
 
103
- > **Note:** Subpath imports such as `@moodlehq/design-system/button` are not currently supported. Please import components from `@moodlehq/design-system`.
103
+ Components can also be imported individually via subpath imports. This is useful when loading files directly by URL (e.g. in Moodle plugins), as only the requested component is fetched:
104
+
105
+ ```js
106
+ import '@moodlehq/design-system/css';
107
+ import { Button } from '@moodlehq/design-system/components/button';
108
+ ```
104
109
 
105
110
  ### Fonts
106
111
 
package/css.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ // Required: TypeScript >= 5.6 with moduleResolution "bundler" and
2
+ // noUncheckedSideEffectImports: true treats a missing `types` condition on a
3
+ // package export as a hard error. This declaration satisfies that check for
4
+ // the side-effect-only CSS import: import '@moodlehq/design-system/css'
5
+ declare module '@moodlehq/design-system/css' {}
@@ -1,10 +1,12 @@
1
- import { IconProp } from '@fortawesome/fontawesome-svg-core';
2
- import { ButtonHTMLAttributes } from 'react';
1
+ import { ButtonHTMLAttributes, ReactElement } from 'react';
2
+ type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'outline-primary' | 'outline-secondary' | 'outline-danger';
3
+ type IconElement = ReactElement<'i' | 'svg'>;
3
4
  export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
4
- label: string;
5
- variant?: string;
5
+ label?: string;
6
+ variant?: ButtonVariant;
6
7
  size?: 'sm' | 'lg';
7
- iconOnly?: boolean;
8
- icon?: IconProp;
8
+ startIcon?: IconElement;
9
+ endIcon?: IconElement;
9
10
  }
10
- export declare const Button: ({ label, variant, size, iconOnly, icon, className, type, ...props }: ButtonProps) => import("react/jsx-runtime").JSX.Element;
11
+ export declare const Button: ({ label, variant, size, startIcon, endIcon, className, type, ...props }: ButtonProps) => import("react/jsx-runtime").JSX.Element;
12
+ export {};
@@ -0,0 +1,37 @@
1
+ import { isValidElement } from "react";
2
+ import { jsxs } from "react/jsx-runtime";
3
+ //#region components/button/Button.tsx
4
+ var isIconElement = (el, propName) => {
5
+ return isValidElement(el) && (el.type === "i" || el.type === "svg");
6
+ };
7
+ var allowedVariants = [
8
+ "primary",
9
+ "secondary",
10
+ "danger",
11
+ "outline-primary",
12
+ "outline-secondary",
13
+ "outline-danger"
14
+ ];
15
+ var Button = ({ label, variant, size, startIcon, endIcon, className, type = "button", ...props }) => {
16
+ const classes = [
17
+ "mds-btn",
18
+ "btn",
19
+ `btn-${variant && allowedVariants.includes(variant) ? variant : "primary"}`
20
+ ];
21
+ if (size) classes.push(`btn-${size}`);
22
+ if (className) classes.push(className);
23
+ return /* @__PURE__ */ jsxs("button", {
24
+ className: classes.join(" "),
25
+ type,
26
+ ...props,
27
+ children: [
28
+ isIconElement(startIcon, "startIcon") ? startIcon : null,
29
+ label,
30
+ isIconElement(endIcon, "endIcon") ? endIcon : null
31
+ ]
32
+ });
33
+ };
34
+ //#endregion
35
+ export { Button };
36
+
37
+ //# sourceMappingURL=Button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Button.js","names":[],"sources":["../../../components/button/Button.tsx"],"sourcesContent":["import type { ButtonHTMLAttributes, ReactElement } from 'react';\nimport { isValidElement } from 'react';\n\ntype ButtonVariant =\n | 'primary'\n | 'secondary'\n | 'danger'\n | 'outline-primary'\n | 'outline-secondary'\n | 'outline-danger';\n\ntype IconElement = ReactElement<'i' | 'svg'>;\n\nexport interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n label?: string;\n variant?: ButtonVariant;\n size?: 'sm' | 'lg';\n startIcon?: IconElement;\n endIcon?: IconElement;\n}\n\n// Runtime guard — prop for icons must be <i> or <svg> elements\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(`Button: \\`${propName}\\` must be an <i> or <svg> element.`);\n }\n return valid;\n};\n\nconst allowedVariants: ButtonVariant[] = [\n 'primary',\n 'secondary',\n 'danger',\n 'outline-primary',\n 'outline-secondary',\n 'outline-danger',\n];\n\nexport const Button = ({\n label,\n variant,\n size,\n startIcon,\n endIcon,\n className,\n type = 'button',\n ...props\n}: ButtonProps) => {\n // Warn in development if button has no accessible name\n if (import.meta.env.DEV) {\n const hasLabel = Boolean(label);\n const hasAriaLabel = 'aria-label' in props;\n if (!hasLabel && !hasAriaLabel) {\n console.warn(\n 'Button: label prop or aria-label attribute is required for accessibility.',\n );\n }\n if (variant && !allowedVariants.includes(variant as ButtonVariant)) {\n console.warn(\n `[MDS Button] Invalid variant \"${variant}\". Falling back to \"primary\". Allowed: ${allowedVariants.join(', ')}`,\n );\n }\n }\n\n const resolvedVariant =\n variant && allowedVariants.includes(variant as ButtonVariant)\n ? variant\n : 'primary';\n\n const classes = ['mds-btn', 'btn', `btn-${resolvedVariant}`];\n if (size) {\n classes.push(`btn-${size}`);\n }\n if (className) {\n classes.push(className);\n }\n\n return (\n <button className={classes.join(' ')} type={type} {...props}>\n {isIconElement(startIcon, 'startIcon') ? startIcon : null}\n {label}\n {isIconElement(endIcon, 'endIcon') ? endIcon : null}\n </button>\n );\n};\n"],"mappings":";;;AAsBA,IAAM,iBAAiB,IAAa,aAAwC;AAK1E,QAJc,eAAe,GAAG,KAAK,GAAG,SAAS,OAAO,GAAG,SAAS;;AAOtE,IAAM,kBAAmC;CACvC;CACA;CACA;CACA;CACA;CACA;CACD;AAED,IAAa,UAAU,EACrB,OACA,SACA,MACA,WACA,SACA,WACA,OAAO,UACP,GAAG,YACc;CAsBjB,MAAM,UAAU;EAAC;EAAW;EAAO,OAJjC,WAAW,gBAAgB,SAAS,QAAyB,GACzD,UACA;EAEsD;AAC5D,KAAI,KACF,SAAQ,KAAK,OAAO,OAAO;AAE7B,KAAI,UACF,SAAQ,KAAK,UAAU;AAGzB,QACE,qBAAC,UAAD;EAAQ,WAAW,QAAQ,KAAK,IAAI;EAAQ;EAAM,GAAI;YAAtD;GACG,cAAc,WAAW,YAAY,GAAG,YAAY;GACpD;GACA,cAAc,SAAS,UAAU,GAAG,UAAU;GACxC"}
@@ -0,0 +1,2 @@
1
+ import { Button } from "./Button.js";
2
+ export { Button };
@@ -0,0 +1,14 @@
1
+ import { ButtonHTMLAttributes } from 'react';
2
+ export interface CloseButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {
3
+ /**
4
+ * Accessible name announced by screen readers for the close button control.
5
+ * Must be a translated string provided by the caller.
6
+ */
7
+ 'aria-label': string;
8
+ /**
9
+ * Visual size variant for the close icon button.
10
+ * Invalid values fall back to the default `md` size at runtime.
11
+ */
12
+ size?: string;
13
+ }
14
+ export declare const CloseButton: ({ "aria-label": ariaLabel, size, className, ...props }: CloseButtonProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,25 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ //#region components/close-button/CloseButton.tsx
3
+ var allowedSizes = [
4
+ "sm",
5
+ "md",
6
+ "lg"
7
+ ];
8
+ var CloseButton = ({ "aria-label": ariaLabel, size, className, ...props }) => {
9
+ const classes = [
10
+ "mds-close-button",
11
+ "btn-close",
12
+ `mds-close-button--${size && allowedSizes.includes(size) ? size : "md"}`
13
+ ];
14
+ if (className) classes.push(className);
15
+ return /* @__PURE__ */ jsx("button", {
16
+ className: classes.join(" "),
17
+ "aria-label": ariaLabel,
18
+ ...props,
19
+ type: "button"
20
+ });
21
+ };
22
+ //#endregion
23
+ export { CloseButton };
24
+
25
+ //# sourceMappingURL=CloseButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CloseButton.js","names":[],"sources":["../../../components/close-button/CloseButton.tsx"],"sourcesContent":["import type { ButtonHTMLAttributes } from 'react';\n\ntype CloseButtonSize = 'sm' | 'md' | 'lg';\n\nexport interface CloseButtonProps extends Omit<\n ButtonHTMLAttributes<HTMLButtonElement>,\n 'type'\n> {\n /**\n * Accessible name announced by screen readers for the close button control.\n * Must be a translated string provided by the caller.\n */\n 'aria-label': string;\n\n /**\n * Visual size variant for the close icon button.\n * Invalid values fall back to the default `md` size at runtime.\n */\n size?: string;\n}\n\nconst allowedSizes: CloseButtonSize[] = ['sm', 'md', 'lg'];\n\nexport const CloseButton = ({\n 'aria-label': ariaLabel,\n size,\n className,\n ...props\n}: CloseButtonProps) => {\n if (\n import.meta.env.DEV &&\n size &&\n !allowedSizes.includes(size as CloseButtonSize)\n ) {\n console.warn(\n `[MDS CloseButton] Invalid size \"${size}\". Falling back to \"md\". Allowed: ${allowedSizes.join(', ')}`,\n );\n }\n\n const resolvedSize =\n size && allowedSizes.includes(size as CloseButtonSize) ? size : 'md';\n\n const classes = [\n 'mds-close-button',\n 'btn-close',\n `mds-close-button--${resolvedSize}`,\n ];\n if (className) {\n classes.push(className);\n }\n\n return (\n <button\n className={classes.join(' ')}\n aria-label={ariaLabel}\n {...props}\n type=\"button\"\n ></button>\n );\n};\n"],"mappings":";;AAqBA,IAAM,eAAkC;CAAC;CAAM;CAAM;CAAK;AAE1D,IAAa,eAAe,EAC1B,cAAc,WACd,MACA,WACA,GAAG,YACmB;CActB,MAAM,UAAU;EACd;EACA;EACA,qBALA,QAAQ,aAAa,SAAS,KAAwB,GAAG,OAAO;EAMjE;AACD,KAAI,UACF,SAAQ,KAAK,UAAU;AAGzB,QACE,oBAAC,UAAD;EACE,WAAW,QAAQ,KAAK,IAAI;EAC5B,cAAY;EACZ,GAAI;EACJ,MAAK;EACG,CAAA"}
@@ -0,0 +1 @@
1
+ export * from './CloseButton';
@@ -0,0 +1,2 @@
1
+ import { CloseButton } from "./CloseButton.js";
2
+ export { CloseButton };
@@ -1,6 +1,6 @@
1
- export { Alert } from './alert';
2
- export type { AlertProps } from './alert';
3
1
  export { Button } from './button';
4
2
  export type { ButtonProps } from './button';
5
- export { Icon } from './icon';
6
- export type { IconProps } from './icon';
3
+ export { CloseButton } from './close-button';
4
+ export type { CloseButtonProps } from './close-button';
5
+ export { Radio } from './radio';
6
+ export type { RadioProps } from './radio';
@@ -0,0 +1,22 @@
1
+ import { InputHTMLAttributes } from 'react';
2
+ export interface RadioProps extends InputHTMLAttributes<HTMLInputElement> {
3
+ /** Visible label text. When hideLabel is true this also serves as the aria-label fallback
4
+ * if no explicit aria-label prop is provided. */
5
+ label?: string;
6
+ /** When true, the visible label element is hidden. The input is still labelled accessibly
7
+ * via aria-label (prop) → label (prop) in that order of precedence. Suppresses
8
+ * invalidFeedback — feedback text requires a visible label to provide context.
9
+ *
10
+ * Use cases: hideLabel is appropriate when the visual label would be redundant or visually cluttered,
11
+ * such as in dense tables where the column header acts as the label, or in icon-only UIs. Always ensure
12
+ * an accessible name is provided via aria-label or label prop. */
13
+ hideLabel?: boolean;
14
+ /** Marks the input as invalid: applies danger border/label colour and sets aria-invalid.
15
+ * Independent of invalidFeedback — invalid styling can be shown without a message. */
16
+ invalid?: boolean;
17
+ /** Pre-translated error message rendered below the label. Requires invalid={true} and
18
+ * hideLabel={false} to be displayed. Only invalid feedback is supported; is-valid
19
+ * and neutral feedback states are intentionally not implemented. */
20
+ invalidFeedback?: string;
21
+ }
22
+ export declare const Radio: import('react').ForwardRefExoticComponent<RadioProps & import('react').RefAttributes<HTMLInputElement>>;
@@ -0,0 +1,47 @@
1
+ import { forwardRef, useId } from "react";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ //#region components/radio/Radio.tsx
4
+ var Radio = forwardRef(({ invalidFeedback, invalid, className, label, hideLabel = false, ...props }, ref) => {
5
+ const generatedId = useId();
6
+ const id = props.id ?? generatedId;
7
+ const isInvalid = !!invalid;
8
+ const classes = ["mds-form-check"];
9
+ if (!hideLabel) classes.push("form-check");
10
+ if (className) classes.push(className);
11
+ const ariaLabel = hideLabel ? props["aria-label"] ?? label : void 0;
12
+ const feedbackId = invalidFeedback && !hideLabel && invalid ? `${id}-feedback` : void 0;
13
+ return /* @__PURE__ */ jsxs("div", {
14
+ className: classes.join(" "),
15
+ children: [
16
+ /* @__PURE__ */ jsx("input", {
17
+ className: [
18
+ "mds-form-check-input",
19
+ "form-check-input",
20
+ isInvalid ? "is-invalid" : ""
21
+ ].filter(Boolean).join(" "),
22
+ type: "radio",
23
+ ref,
24
+ ...props,
25
+ "aria-invalid": isInvalid ? true : void 0,
26
+ "aria-label": ariaLabel,
27
+ "aria-describedby": feedbackId,
28
+ id
29
+ }),
30
+ !hideLabel && /* @__PURE__ */ jsx("label", {
31
+ className: "mds-form-check-label form-check-label",
32
+ htmlFor: id,
33
+ children: label
34
+ }),
35
+ feedbackId && /* @__PURE__ */ jsx("div", {
36
+ id: feedbackId,
37
+ className: "mds-form-check-feedback invalid-feedback",
38
+ children: invalidFeedback
39
+ })
40
+ ]
41
+ });
42
+ });
43
+ Radio.displayName = "Radio";
44
+ //#endregion
45
+ export { Radio };
46
+
47
+ //# sourceMappingURL=Radio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Radio.js","names":[],"sources":["../../../components/radio/Radio.tsx"],"sourcesContent":["import { type InputHTMLAttributes, forwardRef, useId } from 'react';\n\n// Extend InputHTMLAttributes to allow passing any valid input attributes, but also add our custom props like feedback and label.\nexport interface RadioProps extends InputHTMLAttributes<HTMLInputElement> {\n /** Visible label text. When hideLabel is true this also serves as the aria-label fallback\n * if no explicit aria-label prop is provided. */\n label?: string;\n /** When true, the visible label element is hidden. The input is still labelled accessibly\n * via aria-label (prop) → label (prop) in that order of precedence. Suppresses\n * invalidFeedback — feedback text requires a visible label to provide context.\n *\n * Use cases: hideLabel is appropriate when the visual label would be redundant or visually cluttered,\n * such as in dense tables where the column header acts as the label, or in icon-only UIs. Always ensure\n * an accessible name is provided via aria-label or label prop. */\n hideLabel?: boolean;\n /** Marks the input as invalid: applies danger border/label colour and sets aria-invalid.\n * Independent of invalidFeedback — invalid styling can be shown without a message. */\n invalid?: boolean;\n /** Pre-translated error message rendered below the label. Requires invalid={true} and\n * hideLabel={false} to be displayed. Only invalid feedback is supported; is-valid\n * and neutral feedback states are intentionally not implemented. */\n invalidFeedback?: string;\n}\n\nexport const Radio = forwardRef<HTMLInputElement, RadioProps>(\n (\n {\n invalidFeedback,\n invalid,\n className,\n label,\n hideLabel = false,\n ...props\n }: RadioProps,\n ref,\n ) => {\n const generatedId = useId();\n const id = props.id ?? generatedId;\n\n // The invalid state on the input (border, aria-invalid) is independent of\n // hideLabel — a label-less input can still be invalid. Only the feedback text\n // requires a visible label for context and is suppressed when hideLabel is true.\n const isInvalid = !!invalid;\n\n if (import.meta.env.DEV) {\n if (hideLabel && !props['aria-label'] && !label) {\n console.warn(\n 'Radio: label prop or aria-label attribute is required for accessibility when hideLabel is true.',\n );\n }\n if (!hideLabel && !label) {\n console.warn(\n 'Radio: label prop is required when hideLabel is false. An empty label creates an inaccessible form control.',\n );\n }\n if (hideLabel && invalidFeedback) {\n console.warn(\n 'Radio: invalidFeedback is ignored when hideLabel is true. Feedback text requires a visible label to provide context.',\n );\n }\n if (!hideLabel && invalidFeedback && !invalid) {\n console.warn(\n 'Radio: invalidFeedback is provided without invalid={true}. Pass invalid={true} to apply invalid styling alongside the feedback text.',\n );\n }\n }\n // Build the class list for the radio wrapper div. mds-form-check is always applied\n // as a stable hook for consumers; form-check and layout classes are added only when\n // the label is visible (Bootstrap label/feedback styling and grid layout).\n const classes = ['mds-form-check'];\n if (!hideLabel) {\n classes.push('form-check');\n }\n if (className) {\n classes.push(className);\n }\n\n // When the label is hidden, derive aria-label from: caller's aria-label → label prop → undefined (warned above).\n const ariaLabel = hideLabel ? (props['aria-label'] ?? label) : undefined;\n\n // Link the input to its feedback text so screen readers announce the error message.\n // The ID is only generated when feedback will actually be rendered.\n const feedbackId =\n invalidFeedback && !hideLabel && invalid ? `${id}-feedback` : undefined;\n\n return (\n <div className={classes.join(' ')}>\n <input\n className={[\n 'mds-form-check-input',\n 'form-check-input',\n isInvalid ? 'is-invalid' : '',\n ]\n .filter(Boolean)\n .join(' ')}\n type=\"radio\"\n ref={ref}\n {...props}\n aria-invalid={isInvalid ? true : undefined}\n aria-label={ariaLabel}\n aria-describedby={feedbackId}\n id={id} // Ensure we use the generated ID if no ID is provided, so the label can be properly associated with the input for accessibility.\n />\n {!hideLabel && (\n <label className=\"mds-form-check-label form-check-label\" htmlFor={id}>\n {label}\n </label>\n )}\n {feedbackId && (\n <div\n id={feedbackId}\n className=\"mds-form-check-feedback invalid-feedback\"\n >\n {invalidFeedback}\n </div>\n )}\n </div>\n );\n },\n);\nRadio.displayName = 'Radio';\n"],"mappings":";;;AAwBA,IAAa,QAAQ,YAEjB,EACE,iBACA,SACA,WACA,OACA,YAAY,OACZ,GAAG,SAEL,QACG;CACH,MAAM,cAAc,OAAO;CAC3B,MAAM,KAAK,MAAM,MAAM;CAKvB,MAAM,YAAY,CAAC,CAAC;CA2BpB,MAAM,UAAU,CAAC,iBAAiB;AAClC,KAAI,CAAC,UACH,SAAQ,KAAK,aAAa;AAE5B,KAAI,UACF,SAAQ,KAAK,UAAU;CAIzB,MAAM,YAAY,YAAa,MAAM,iBAAiB,QAAS,KAAA;CAI/D,MAAM,aACJ,mBAAmB,CAAC,aAAa,UAAU,GAAG,GAAG,aAAa,KAAA;AAEhE,QACE,qBAAC,OAAD;EAAK,WAAW,QAAQ,KAAK,IAAI;YAAjC;GACE,oBAAC,SAAD;IACE,WAAW;KACT;KACA;KACA,YAAY,eAAe;KAC5B,CACE,OAAO,QAAQ,CACf,KAAK,IAAI;IACZ,MAAK;IACA;IACL,GAAI;IACJ,gBAAc,YAAY,OAAO,KAAA;IACjC,cAAY;IACZ,oBAAkB;IACd;IACJ,CAAA;GACD,CAAC,aACA,oBAAC,SAAD;IAAO,WAAU;IAAwC,SAAS;cAC/D;IACK,CAAA;GAET,cACC,oBAAC,OAAD;IACE,IAAI;IACJ,WAAU;cAET;IACG,CAAA;GAEJ;;EAGX;AACD,MAAM,cAAc"}
@@ -0,0 +1 @@
1
+ export * from './Radio';
@@ -0,0 +1,2 @@
1
+ import { Radio } from "./Radio.js";
2
+ export { Radio };