@madecki/ui 2.0.0 → 2.1.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [2.1.0](https://github.com/madecki/ui/compare/v2.0.0...v2.1.0) (2026-04-08)
2
+
3
+ ### Features
4
+
5
+ * add DetailsPanel native details/summary disclosure ([d1d24d9](https://github.com/madecki/ui/commit/d1d24d989537fe1845aeb5059b3f59d745357c23))
6
+ * add Toast with placement, auto-close, and variants ([5874709](https://github.com/madecki/ui/commit/587470962cf265eafe8cc9659b78b073f6ef1756))
7
+
1
8
  ## [2.0.0](https://github.com/madecki/ui/compare/v1.5.0...v2.0.0) (2026-04-08)
2
9
 
3
10
  ### ⚠ BREAKING CHANGES
package/README.md CHANGED
@@ -424,6 +424,47 @@ Full-screen loading overlay.
424
424
  <SpinnerOverlay isVisible={isLoading} />
425
425
  ```
426
426
 
427
+ #### Toast
428
+
429
+ Fixed-position message in any screen corner, with configurable auto-dismiss (milliseconds) and a close control. Border colors match semantic tokens (`info` uses the same blue accent as `ContentBox`).
430
+
431
+ ```tsx
432
+ import { Toast } from "@madecki/ui";
433
+ import { useState } from "react";
434
+
435
+ function Example() {
436
+ const [toastKey, setToastKey] = useState(0);
437
+
438
+ return (
439
+ <>
440
+ <button type="button" onClick={() => setToastKey((n) => n + 1)}>
441
+ Save
442
+ </button>
443
+ {toastKey > 0 ? (
444
+ <Toast
445
+ key={toastKey}
446
+ variant="success"
447
+ placement="bottom-right"
448
+ autoCloseMs={4000}
449
+ onClose={() => setToastKey(0)}
450
+ >
451
+ <p className="text-white">Saved.</p>
452
+ </Toast>
453
+ ) : null}
454
+ </>
455
+ );
456
+ }
457
+ ```
458
+
459
+ | Prop | Type | Default | Description |
460
+ |------|------|---------|-------------|
461
+ | `children` | `ReactNode` | (required) | Message body (use `Text` / markup as needed). |
462
+ | `variant` | `"info"` \| `"success"` \| `"danger"` | `"info"` | Semantic style (border / live region). |
463
+ | `placement` | `"top-left"` \| `"top-right"` \| `"bottom-left"` \| `"bottom-right"` | `"bottom-right"` | Viewport corner (`fixed`, offset with spacing token `5`). |
464
+ | `autoCloseMs` | `number` | `5000` | Auto-dismiss delay in ms; use `0` to disable. |
465
+ | `onClose` | `() => void` | — | Fired when the toast is dismissed (timer or close control). |
466
+ | `className` | `string` | `""` | Extra classes on the root element. |
467
+
427
468
  ### Content Components
428
469
 
429
470
  #### BlockQuote
@@ -452,6 +493,37 @@ import { ContentBox, Info, Warning } from "@madecki/ui";
452
493
  </ContentBox>
453
494
  ```
454
495
 
496
+ #### DetailsPanel
497
+
498
+ Collapsible panel using native **`<details>`** and **`<summary>`** (accessible disclosure), with the same border variants as `ContentBox`.
499
+
500
+ ```tsx
501
+ import { DetailsPanel, Info } from "@madecki/ui";
502
+
503
+ <DetailsPanel variant="info" summary="Show more">
504
+ <p className="text-white">Extra content appears here when expanded.</p>
505
+ </DetailsPanel>
506
+
507
+ <DetailsPanel
508
+ variant="warning"
509
+ icon={<Info />}
510
+ defaultOpen
511
+ summary="Important details"
512
+ >
513
+ <p className="text-white">Initially open; still toggles like a normal details element.</p>
514
+ </DetailsPanel>
515
+ ```
516
+
517
+ | Prop | Type | Default | Description |
518
+ |------|------|---------|-------------|
519
+ | `summary` | `ReactNode` | (required) | Content of the `<summary>` row (click to toggle). |
520
+ | `children` | `ReactNode` | (required) | Panel body shown when open. |
521
+ | `variant` | `"info"` \| `"warning"` \| `"success"` \| `"danger"` | `"info"` | Border color (matches `ContentBox`). |
522
+ | `icon` | `ReactNode` | — | Optional icon before the summary text. |
523
+ | `defaultOpen` | `boolean` | `false` | Initial open state; toggling still uses native `<details>` behavior (open state is synced in React so `defaultOpen` works reliably). |
524
+ | `className` | `string` | `""` | Extra classes on `<details>`. |
525
+ | `id` | `string` | — | `id` on `<details>`. |
526
+
455
527
  #### Hr
456
528
 
457
529
  Styled horizontal rule.
@@ -589,6 +661,7 @@ There is **no** separate manual changelog step: if it is not described in a merg
589
661
  This project uses [Conventional Commits](https://www.conventionalcommits.org/) enforced by [commitlint](https://commitlint.js.org/):
590
662
 
591
663
  - **Locally:** Husky runs **`commitlint`** on the **`commit-msg`** hook, so **`git commit` fails** if the message does not follow the rules (bypass only with **`git commit --no-verify`**).
664
+ - **Locally:** Husky runs **`npm run precheck`** on **`pre-push`** (typecheck, lint, build, tests, Storybook build — same as CI/Release). Bypass with **`git push --no-verify`** if you must.
592
665
  - **On `main`:** CI runs commitlint on **every push** for the commits in that push, so bad messages are caught even if someone skipped the hook.
593
666
 
594
667
  **Format:** `type(scope?): description`
@@ -44,7 +44,7 @@ This file contains:
44
44
  ## Import Pattern
45
45
 
46
46
  \`\`\`tsx
47
- import { Container, Stack, Heading, Text, Button } from "@madecki/ui";
47
+ import { Container, Stack, Heading, Text, Button, DetailsPanel, Toast } from "@madecki/ui";
48
48
  import { Heart, Info } from "@madecki/ui";
49
49
  \`\`\`
50
50
 
package/dist/index.cjs CHANGED
@@ -1014,6 +1014,93 @@ var SpinnerOverlay = ({
1014
1014
  }
1015
1015
  );
1016
1016
  };
1017
+
1018
+ // src/components/contentBoxVariants.ts
1019
+ var contentBoxVariantBorderClasses = {
1020
+ info: "border-blue",
1021
+ warning: "border-warning",
1022
+ success: "border-success",
1023
+ danger: "border-danger"
1024
+ };
1025
+ var placementClassNames = {
1026
+ "top-left": "top-5 left-5",
1027
+ "top-right": "top-5 right-5",
1028
+ "bottom-left": "bottom-5 left-5",
1029
+ "bottom-right": "bottom-5 right-5"
1030
+ };
1031
+ function CloseIcon({ className = "" }) {
1032
+ return /* @__PURE__ */ jsxRuntime.jsx(
1033
+ "svg",
1034
+ {
1035
+ width: 16,
1036
+ height: 16,
1037
+ viewBox: "0 0 16 16",
1038
+ fill: "none",
1039
+ xmlns: "http://www.w3.org/2000/svg",
1040
+ className,
1041
+ "aria-hidden": true,
1042
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1043
+ "path",
1044
+ {
1045
+ d: "M4 4L12 12M12 4L4 12",
1046
+ stroke: "currentColor",
1047
+ strokeWidth: "2",
1048
+ strokeLinecap: "round"
1049
+ }
1050
+ )
1051
+ }
1052
+ );
1053
+ }
1054
+ var Toast = ({
1055
+ children,
1056
+ variant = "info",
1057
+ placement = "bottom-right",
1058
+ autoCloseMs = 5e3,
1059
+ onClose,
1060
+ className = ""
1061
+ }) => {
1062
+ const [open, setOpen] = react.useState(true);
1063
+ const onCloseRef = react.useRef(onClose);
1064
+ onCloseRef.current = onClose;
1065
+ const dismiss = react.useCallback(() => {
1066
+ setOpen(false);
1067
+ onCloseRef.current?.();
1068
+ }, []);
1069
+ react.useEffect(() => {
1070
+ if (!open || autoCloseMs <= 0) {
1071
+ return;
1072
+ }
1073
+ const id = window.setTimeout(dismiss, autoCloseMs);
1074
+ return () => window.clearTimeout(id);
1075
+ }, [open, autoCloseMs, dismiss]);
1076
+ if (!open) {
1077
+ return null;
1078
+ }
1079
+ const border = contentBoxVariantBorderClasses[variant];
1080
+ const live = variant === "danger" ? "assertive" : "polite";
1081
+ const role = variant === "danger" ? "alert" : "status";
1082
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1083
+ "div",
1084
+ {
1085
+ role,
1086
+ "aria-live": live,
1087
+ className: `fixed z-50 flex max-w-sm gap-3 rounded-md border-2 bg-darkgray p-5 text-md text-white shadow-lg dark:bg-gray ${placementClassNames[placement]} ${border} ${className}`,
1088
+ children: [
1089
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-w-0 flex-1", children }),
1090
+ /* @__PURE__ */ jsxRuntime.jsx(
1091
+ "button",
1092
+ {
1093
+ type: "button",
1094
+ onClick: dismiss,
1095
+ className: "shrink-0 rounded-sm p-2 text-icongray transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue",
1096
+ "aria-label": "Close",
1097
+ children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, {})
1098
+ }
1099
+ )
1100
+ ]
1101
+ }
1102
+ );
1103
+ };
1017
1104
  var BlockQuote = ({ children, className = "" }) => {
1018
1105
  return /* @__PURE__ */ jsxRuntime.jsx(
1019
1106
  "blockquote",
@@ -1027,12 +1114,6 @@ var Hr = ({ className = "" }) => {
1027
1114
  const defaultClassNames = "border border-gray my-4";
1028
1115
  return /* @__PURE__ */ jsxRuntime.jsx("hr", { className: `${defaultClassNames} ${className}` });
1029
1116
  };
1030
- var variantStyles = {
1031
- info: "border-blue",
1032
- warning: "border-warning",
1033
- success: "border-success",
1034
- danger: "border-danger"
1035
- };
1036
1117
  var ContentBox = ({
1037
1118
  children,
1038
1119
  variant = "info",
@@ -1042,7 +1123,7 @@ var ContentBox = ({
1042
1123
  return /* @__PURE__ */ jsxRuntime.jsxs(
1043
1124
  "div",
1044
1125
  {
1045
- className: `relative border rounded-md my-9 ${variantStyles[variant]} ${className}`,
1126
+ className: `relative border rounded-md my-9 ${contentBoxVariantBorderClasses[variant]} ${className}`,
1046
1127
  children: [
1047
1128
  icon && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute flex items-center justify-center p-3 -top-6 right-8 bg-primary border border-darkgray", children: icon }),
1048
1129
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "m-7", children })
@@ -1050,6 +1131,65 @@ var ContentBox = ({
1050
1131
  }
1051
1132
  );
1052
1133
  };
1134
+ function ChevronDown2({ className = "" }) {
1135
+ return /* @__PURE__ */ jsxRuntime.jsx(
1136
+ "svg",
1137
+ {
1138
+ width: 20,
1139
+ height: 20,
1140
+ viewBox: "0 0 20 20",
1141
+ fill: "none",
1142
+ xmlns: "http://www.w3.org/2000/svg",
1143
+ className,
1144
+ "aria-hidden": true,
1145
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1146
+ "path",
1147
+ {
1148
+ d: "M5 7.5L10 12.5L15 7.5",
1149
+ stroke: "currentColor",
1150
+ strokeWidth: "1.5",
1151
+ strokeLinecap: "round",
1152
+ strokeLinejoin: "round"
1153
+ }
1154
+ )
1155
+ }
1156
+ );
1157
+ }
1158
+ var DetailsPanel = ({
1159
+ summary,
1160
+ children,
1161
+ variant = "info",
1162
+ icon,
1163
+ defaultOpen = false,
1164
+ className = "",
1165
+ id
1166
+ }) => {
1167
+ const border = contentBoxVariantBorderClasses[variant];
1168
+ const [open, setOpen] = react.useState(defaultOpen);
1169
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1170
+ "details",
1171
+ {
1172
+ id,
1173
+ className: `group relative border rounded-md my-9 ${border} ${className}`,
1174
+ open,
1175
+ onToggle: (e) => setOpen(e.currentTarget.open),
1176
+ children: [
1177
+ /* @__PURE__ */ jsxRuntime.jsxs(
1178
+ "summary",
1179
+ {
1180
+ className: "flex cursor-pointer list-none items-center gap-3 p-7 text-white outline-none transition-colors hover:bg-darkgray/40 dark:hover:bg-white/5 focus-visible:ring-2 focus-visible:ring-blue group-open:pb-3 [&::-webkit-details-marker]:hidden",
1181
+ children: [
1182
+ /* @__PURE__ */ jsxRuntime.jsx(ChevronDown2, { className: "size-5 shrink-0 text-icongray transition-transform group-open:rotate-180" }),
1183
+ icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex shrink-0 items-center", children: icon }) : null,
1184
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 flex-1 text-md font-medium text-white dark:text-white", children: summary })
1185
+ ]
1186
+ }
1187
+ ),
1188
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-gray px-7 pb-7 pt-3 dark:border-gray", children })
1189
+ ]
1190
+ }
1191
+ );
1192
+ };
1053
1193
  var sizeStyles2 = {
1054
1194
  sm: "max-w-screen-sm",
1055
1195
  md: "max-w-screen-md",
@@ -1587,6 +1727,7 @@ exports.Button = Button;
1587
1727
  exports.ButtonTransparent = ButtonTransparent;
1588
1728
  exports.Container = Container;
1589
1729
  exports.ContentBox = ContentBox;
1730
+ exports.DetailsPanel = DetailsPanel;
1590
1731
  exports.GradientButton = GradientButton;
1591
1732
  exports.Grid = Grid;
1592
1733
  exports.GridItem = GridItem;
@@ -1608,6 +1749,7 @@ exports.Tabs = Tabs;
1608
1749
  exports.Tag = Tag;
1609
1750
  exports.Text = Text;
1610
1751
  exports.Textarea = Textarea;
1752
+ exports.Toast = Toast;
1611
1753
  exports.TwitterIcon = TwitterIcon;
1612
1754
  exports.Warning = Warning;
1613
1755
  //# sourceMappingURL=index.cjs.map