@lablup/ui-common 0.1.0-alpha.10 → 0.1.0-alpha.12
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/dist/assets/components/Drawer/Drawer.css +1 -1
- package/dist/assets/components/PageHeader/PageHeader.css +1 -1
- package/dist/components/Drawer/Drawer.d.ts +12 -1
- package/dist/components/Drawer/Drawer2.js +40 -24
- package/dist/components/Drawer/Drawer2.js.map +1 -1
- package/dist/components/PageHeader/PageHeader.d.ts +14 -1
- package/dist/components/PageHeader/PageHeader2.js +40 -24
- package/dist/components/PageHeader/PageHeader2.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
.drawer__backdrop.drawer__backdrop{background-color:var(--token-colorBgMask,#00000073);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);z-index:var(--token-zIndexDrawer,1100);pointer-events:none;visibility:hidden;opacity:0;transition:opacity var(--token-motionDurationMid,.3s) ease, visibility 0s linear var(--token-motionDurationMid,.3s), background-color var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out), backdrop-filter var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out);position:fixed;inset:0}.drawer__backdrop--open.drawer__backdrop--open{pointer-events:auto;visibility:visible;opacity:1;transition:opacity var(--token-motionDurationMid,.3s) ease, visibility 0s linear 0s, background-color var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out), backdrop-filter var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out)}.drawer.drawer{background-color:var(--token-colorBgContainer,#fff);box-shadow:var(--token-boxShadowDrawerRight,-4px 0 20px #00000026);opacity:0;transition:transform var(--token-motionDurationMid,.3s) cubic-bezier(.4, 0, .2, 1), opacity var(--token-motionDurationMid,.3s) ease, background-color var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out), color var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out), border-color var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out), box-shadow var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out);z-index:var(--token-zIndexDrawerContent,1101);flex-direction:column;display:flex;position:fixed;top:0;bottom:0;right:0;transform:translate(100%)}.drawer.drawer--open{opacity:1;transform:translate(0)}.drawer__header{padding:var(--token-paddingLG,1.5rem);border-bottom:1px solid var(--token-colorBorder,#d9d9d9);flex-shrink:0}.drawer__title-row{justify-content:space-between;align-items:center;gap:var(--token-paddingMD,1rem);display:flex}.drawer__title{font-size:var(--token-fontSizeHeading4,1.25rem);color:var(--token-colorText,#141414);margin:0;font-weight:600}.drawer__subtitle{font-size:var(--token-fontSizeSM,.875rem);color:var(--token-colorTextSecondary,#6b7280);margin:var(--token-paddingSM,.5rem) 0 0 0;text-overflow:ellipsis;overflow:hidden}.drawer__close-btn.drawer__close-btn{border-radius:var(--token-borderRadius,6px);width:2rem;height:2rem;color:var(--token-colorTextSecondary,#6b7280);cursor:pointer;transition:background-color var(--token-motionDurationFast,.15s) ease, color var(--token-motionDurationFast,.15s) ease, border-color var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out);background:0 0;border:none;flex-shrink:0;justify-content:center;align-items:center;display:flex}.drawer__close-btn:hover{background-color:var(--token-colorFillSecondary,#f3f4f6);color:var(--token-colorText,#141414)}.drawer__close-btn:focus-visible{outline:var(--token-focusRingWidth,2px) var(--token-focusRingStyle,solid) var(--token-focusRingColor,var(--token-colorPrimary));outline-offset:var(--token-focusRingOffset,2px)}.drawer__content{flex:1;overflow:hidden auto}.drawer__footer{padding:var(--token-paddingMD,1rem) var(--token-paddingLG,1.5rem);border-top:1px solid var(--token-colorBorder,#d9d9d9);background-color:var(--token-colorFillSecondary,#f3f4f6);flex-shrink:0}@media (prefers-reduced-motion:reduce){.drawer__backdrop.drawer__backdrop,.drawer.drawer,.drawer__close-btn.drawer__close-btn,.drawer__backdrop--open.drawer__backdrop--open{transition:none}}@media (prefers-reduced-transparency:reduce){.drawer__backdrop.drawer__backdrop{-webkit-backdrop-filter:none;background-color:#000000a6}}@media (max-width:767px){.drawer{width:100vw!important;max-width:100vw!important}.drawer__header,.drawer__footer{padding:var(--token-paddingMD,1rem)}}
|
|
1
|
+
.drawer__backdrop.drawer__backdrop{background-color:var(--token-colorBgMask,#00000073);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);z-index:var(--token-zIndexDrawer,1100);pointer-events:none;visibility:hidden;opacity:0;transition:opacity var(--token-motionDurationMid,.3s) ease, visibility 0s linear var(--token-motionDurationMid,.3s), background-color var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out), backdrop-filter var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out);position:fixed;inset:0}.drawer__backdrop--open.drawer__backdrop--open{pointer-events:auto;visibility:visible;opacity:1;transition:opacity var(--token-motionDurationMid,.3s) ease, visibility 0s linear 0s, background-color var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out), backdrop-filter var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out)}.drawer.drawer{background-color:var(--token-colorBgContainer,#fff);box-shadow:var(--token-boxShadowDrawerRight,-4px 0 20px #00000026);opacity:0;transition:transform var(--token-motionDurationMid,.3s) cubic-bezier(.4, 0, .2, 1), opacity var(--token-motionDurationMid,.3s) ease, background-color var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out), color var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out), border-color var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out), box-shadow var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out);z-index:var(--token-zIndexDrawerContent,1101);flex-direction:column;display:flex;position:fixed;top:0;bottom:0;right:0;transform:translate(100%)}.drawer.drawer--open{opacity:1;transform:translate(0)}.drawer__header{padding:var(--token-paddingLG,1.5rem);border-bottom:1px solid var(--token-colorBorder,#d9d9d9);flex-shrink:0}.drawer__title-row{justify-content:space-between;align-items:center;gap:var(--token-paddingMD,1rem);display:flex}.drawer__title{font-size:var(--token-fontSizeHeading4,1.25rem);color:var(--token-colorText,#141414);margin:0;font-weight:600}.drawer__subtitle{font-size:var(--token-fontSizeSM,.875rem);color:var(--token-colorTextSecondary,#6b7280);margin:var(--token-paddingSM,.5rem) 0 0 0;text-overflow:ellipsis;overflow:hidden}.drawer__close-btn.drawer__close-btn{border-radius:var(--token-borderRadius,6px);width:2rem;height:2rem;color:var(--token-colorTextSecondary,#6b7280);cursor:pointer;transition:background-color var(--token-motionDurationFast,.15s) ease, color var(--token-motionDurationFast,.15s) ease, border-color var(--token-themeTransitionDuration,.25s) var(--token-themeTransitionTiming,ease-in-out);background:0 0;border:none;flex-shrink:0;justify-content:center;align-items:center;display:flex}.drawer__close-btn:hover{background-color:var(--token-colorFillSecondary,#f3f4f6);color:var(--token-colorText,#141414)}.drawer__close-btn:focus-visible{outline:var(--token-focusRingWidth,2px) var(--token-focusRingStyle,solid) var(--token-focusRingColor,var(--token-colorPrimary));outline-offset:var(--token-focusRingOffset,2px)}.drawer__content{flex:1;overflow:hidden auto}.drawer__footer{padding:var(--token-paddingMD,1rem) var(--token-paddingLG,1.5rem);border-top:1px solid var(--token-colorBorder,#d9d9d9);background-color:var(--token-colorFillSecondary,#f3f4f6);flex-shrink:0}@media (prefers-reduced-motion:reduce){.drawer__backdrop.drawer__backdrop,.drawer.drawer,.drawer__close-btn.drawer__close-btn,.drawer__backdrop--open.drawer__backdrop--open{transition:none}}@media (prefers-reduced-transparency:reduce){.drawer__backdrop.drawer__backdrop{-webkit-backdrop-filter:none;background-color:#000000a6}}@media (max-width:767px){.drawer{width:100vw!important;max-width:100vw!important}.drawer__header,.drawer__footer{padding:var(--token-paddingMD,1rem)}}@keyframes drawer-shake{0%,to{transform:translate(0)}10%,30%,50%,70%,90%{transform:translate(-4px)}20%,40%,60%,80%{transform:translate(4px)}}.drawer--shaking{animation:.4s ease-in-out drawer-shake}@media (prefers-reduced-motion:reduce){.drawer--shaking{animation:none}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.page-header{margin-bottom:var(--token-paddingLG,1.5rem)}.page-header__content{justify-content:space-between;align-items:flex-start;gap:var(--token-paddingMD,1rem);flex-wrap:wrap;display:flex}.page-header__text{flex:20rem;min-width:min(100%,12rem)}.page-header__title{margin:0 0 var(--token-paddingSM,.5rem);font-size:var(--token-fontSizeDisplay4,1.75rem);letter-spacing:.02em;color:var(--token-colorText,#141414);font-weight:300}.page-header__description{font-size:var(--token-fontSize,.875rem);color:var(--token-colorTextSecondary,#595959);margin:0}.page-header__actions{justify-content:flex-end;align-items:flex-start;gap:var(--token-paddingSM,.5rem);flex-wrap:wrap;flex:0 auto;min-width:0;margin-left:auto;display:flex}.page-header__actions>*{flex-shrink:0}.page-header__error{justify-content:space-between;align-items:center;gap:var(--token-paddingSM,.5rem);margin-top:var(--token-paddingSM,.5rem);padding:var(--token-paddingSM,.5rem) var(--token-paddingMD,1rem);font-size:var(--token-fontSize,.875rem);color:var(--token-colorError,#c82333);border-radius:var(--token-borderRadiusLG,.5rem);background-color:#ff4d4f14;border:1px solid #ff4d4f4d;display:flex}.page-header__error-text{word-break:break-word;flex:1}.page-header__error-dismiss{font-size:var(--token-fontSizeXL,1.25rem);color:currentColor;cursor:pointer;opacity:.7;width:24px;height:24px;transition:opacity var(--token-motionDurationFast,.1s);background:0 0;border:none;flex-shrink:0;justify-content:center;align-items:center;padding:0;line-height:1;display:flex}.page-header__error-dismiss:hover{opacity:1}@media (max-width:767px){.page-header__content{flex-direction:column;align-items:stretch}.page-header__text{flex:none;min-width:0}.page-header__actions{justify-content:flex-end}}
|
|
1
|
+
.page-header{margin-bottom:var(--token-paddingLG,1.5rem)}.page-header__content{justify-content:space-between;align-items:flex-start;gap:var(--token-paddingMD,1rem);flex-wrap:wrap;display:flex}.page-header__text{flex:20rem;min-width:min(100%,12rem)}.page-header__title{margin:0 0 var(--token-paddingSM,.5rem);font-size:var(--token-fontSizeDisplay4,1.75rem);letter-spacing:.02em;color:var(--token-colorText,#141414);font-weight:300}.page-header__description{font-size:var(--token-fontSize,.875rem);color:var(--token-colorTextSecondary,#595959);margin:0}.page-header__actions{justify-content:flex-end;align-items:flex-start;gap:var(--token-paddingSM,.5rem);flex-wrap:wrap;flex:0 auto;min-width:0;margin-left:auto;display:flex}.page-header__actions>*{flex-shrink:0}.page-header__error{justify-content:space-between;align-items:center;gap:var(--token-paddingSM,.5rem);margin-top:var(--token-paddingSM,.5rem);padding:var(--token-paddingSM,.5rem) var(--token-paddingMD,1rem);font-size:var(--token-fontSize,.875rem);color:var(--token-colorError,#c82333);border-radius:var(--token-borderRadiusLG,.5rem);background-color:#ff4d4f14;border:1px solid #ff4d4f4d;display:flex}.page-header__error-text{word-break:break-word;flex:1}.page-header__error-dismiss{font-size:var(--token-fontSizeXL,1.25rem);color:currentColor;cursor:pointer;opacity:.7;width:24px;height:24px;transition:opacity var(--token-motionDurationFast,.1s);background:0 0;border:none;flex-shrink:0;justify-content:center;align-items:center;padding:0;line-height:1;display:flex}.page-header__error-dismiss:hover{opacity:1}@media (max-width:767px){.page-header__content{flex-direction:column;align-items:stretch}.page-header__text{flex:none;min-width:0}.page-header__actions{justify-content:flex-end}}.page-header__error-body{gap:var(--token-paddingXXS,.125rem);flex-direction:column;flex:1;min-width:0;display:flex}.page-header__error-detail{font-size:var(--token-fontSizeSM,.75rem);color:var(--token-colorTextTertiary,#737373);word-break:break-word}.page-header__error-actions{align-items:center;gap:var(--token-paddingSM,.5rem);padding-left:var(--token-paddingXS,.25rem);flex-shrink:0;display:flex}
|
|
@@ -21,5 +21,16 @@ export interface DrawerProps {
|
|
|
21
21
|
ariaDescribedBy?: string;
|
|
22
22
|
/** Accessible label for the close button. Default: "Close" */
|
|
23
23
|
closeLabel?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Refuse dismissal by Escape or a backdrop click, shaking the panel instead.
|
|
26
|
+
* The close button and any footer control still call `onClose` directly, so
|
|
27
|
+
* route those through the same guard when they also need confirming.
|
|
28
|
+
*/
|
|
29
|
+
preventDismiss?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Called when `preventDismiss` blocked a dismissal, so the consumer can offer
|
|
32
|
+
* an explicit way out (a discard confirmation, for instance).
|
|
33
|
+
*/
|
|
34
|
+
onDismissAttempt?: () => void;
|
|
24
35
|
}
|
|
25
|
-
export declare function Drawer({ isOpen, onClose, title, subtitle, width, children, footer, className, ariaLabelledBy, ariaDescribedBy, closeLabel, }: DrawerProps): import("react").JSX.Element;
|
|
36
|
+
export declare function Drawer({ isOpen, onClose, title, subtitle, width, children, footer, className, ariaLabelledBy, ariaDescribedBy, closeLabel, preventDismiss, onDismissAttempt, }: DrawerProps): import("react").JSX.Element;
|
|
@@ -7,65 +7,81 @@ var o = {
|
|
|
7
7
|
medium: "520px",
|
|
8
8
|
wide: "900px"
|
|
9
9
|
};
|
|
10
|
-
function s({ isOpen: s, onClose: c, title: l, subtitle: u, width: d = "medium", children: f, footer: p, className: m = "", ariaLabelledBy: h, ariaDescribedBy: g, closeLabel: _ = "Close" }) {
|
|
11
|
-
let
|
|
10
|
+
function s({ isOpen: s, onClose: c, title: l, subtitle: u, width: d = "medium", children: f, footer: p, className: m = "", ariaLabelledBy: h, ariaDescribedBy: g, closeLabel: _ = "Close", preventDismiss: v = !1, onDismissAttempt: y }) {
|
|
11
|
+
let b = n(null), [x, S] = r(!1), C = n(v);
|
|
12
|
+
C.current = v;
|
|
13
|
+
let w = n(y);
|
|
14
|
+
w.current = y;
|
|
15
|
+
let T = e(() => C.current ? (S(!0), w.current?.(), !0) : !1, []), E = e((e) => {
|
|
16
|
+
e.animationName === "drawer-shake" && S(!1);
|
|
17
|
+
}, []), D = n(null), O = n(null), [k, A] = r(!1), j = n(!1);
|
|
12
18
|
t(() => {
|
|
13
|
-
s && !
|
|
19
|
+
s && !j.current ? (j.current = !0, requestAnimationFrame(() => {
|
|
14
20
|
requestAnimationFrame(() => {
|
|
15
|
-
|
|
21
|
+
A(!0);
|
|
16
22
|
});
|
|
17
|
-
})) :
|
|
23
|
+
})) : A(!!s);
|
|
18
24
|
}, [s]);
|
|
19
|
-
let
|
|
25
|
+
let M = s && k, N = d in o ? o[d] : d, P = h || "drawer-title", F = g || (u ? "drawer-subtitle" : void 0);
|
|
20
26
|
t(() => {
|
|
21
27
|
if (!s) return;
|
|
22
|
-
|
|
23
|
-
let e =
|
|
28
|
+
D.current = document.activeElement;
|
|
29
|
+
let e = b.current;
|
|
24
30
|
if (!e) return;
|
|
25
31
|
requestAnimationFrame(() => {
|
|
26
|
-
|
|
32
|
+
O.current?.focus();
|
|
27
33
|
});
|
|
28
34
|
let t = e.querySelectorAll("button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])"), n = t[0], r = t[t.length - 1], i = (e) => {
|
|
29
35
|
if (e.key === "Escape") {
|
|
36
|
+
if (T()) return;
|
|
30
37
|
c();
|
|
31
38
|
return;
|
|
32
39
|
}
|
|
33
40
|
e.key === "Tab" && (e.shiftKey ? document.activeElement === n && (e.preventDefault(), r?.focus()) : document.activeElement === r && (e.preventDefault(), n?.focus()));
|
|
34
41
|
};
|
|
35
42
|
return e.addEventListener("keydown", i), () => {
|
|
36
|
-
e.removeEventListener("keydown", i),
|
|
43
|
+
e.removeEventListener("keydown", i), D.current && D.current.focus();
|
|
37
44
|
};
|
|
38
|
-
}, [
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
}, [
|
|
46
|
+
s,
|
|
47
|
+
c,
|
|
48
|
+
T
|
|
49
|
+
]);
|
|
50
|
+
let I = e((e) => {
|
|
51
|
+
if (e.target === e.currentTarget) {
|
|
52
|
+
if (T()) return;
|
|
53
|
+
c();
|
|
54
|
+
}
|
|
55
|
+
}, [c, T]);
|
|
42
56
|
return /* @__PURE__ */ i("div", {
|
|
43
|
-
className: `drawer__backdrop ${
|
|
44
|
-
onClick:
|
|
57
|
+
className: `drawer__backdrop ${M ? "drawer__backdrop--open" : ""}`,
|
|
58
|
+
onClick: I,
|
|
45
59
|
role: "presentation",
|
|
46
60
|
"aria-hidden": !s,
|
|
61
|
+
inert: !s,
|
|
47
62
|
children: /* @__PURE__ */ a("aside", {
|
|
48
|
-
ref:
|
|
49
|
-
className: `drawer ${
|
|
63
|
+
ref: b,
|
|
64
|
+
className: `drawer ${M ? "drawer--open" : ""} ${x ? "drawer--shaking" : ""} ${m}`,
|
|
50
65
|
style: {
|
|
51
|
-
width:
|
|
66
|
+
width: N,
|
|
52
67
|
maxWidth: "100vw"
|
|
53
68
|
},
|
|
54
69
|
role: "dialog",
|
|
55
70
|
"aria-modal": "true",
|
|
56
|
-
"aria-labelledby":
|
|
57
|
-
"aria-describedby":
|
|
71
|
+
"aria-labelledby": P,
|
|
72
|
+
"aria-describedby": F,
|
|
73
|
+
onAnimationEnd: E,
|
|
58
74
|
children: [
|
|
59
75
|
/* @__PURE__ */ a("header", {
|
|
60
76
|
className: "drawer__header",
|
|
61
77
|
children: [/* @__PURE__ */ a("div", {
|
|
62
78
|
className: "drawer__title-row",
|
|
63
79
|
children: [/* @__PURE__ */ i("h2", {
|
|
64
|
-
id:
|
|
80
|
+
id: P,
|
|
65
81
|
className: "drawer__title",
|
|
66
82
|
children: l
|
|
67
83
|
}), /* @__PURE__ */ i("button", {
|
|
68
|
-
ref:
|
|
84
|
+
ref: O,
|
|
69
85
|
type: "button",
|
|
70
86
|
className: "drawer__close-btn",
|
|
71
87
|
onClick: c,
|
|
@@ -83,7 +99,7 @@ function s({ isOpen: s, onClose: c, title: l, subtitle: u, width: d = "medium",
|
|
|
83
99
|
})
|
|
84
100
|
})]
|
|
85
101
|
}), u && /* @__PURE__ */ i("p", {
|
|
86
|
-
id:
|
|
102
|
+
id: F,
|
|
87
103
|
className: "drawer__subtitle",
|
|
88
104
|
children: u
|
|
89
105
|
})]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Drawer2.js","names":[],"sources":["../../../src/components/Drawer/Drawer.tsx"],"sourcesContent":["/**\n * Drawer Component\n *\n * A reusable right-side drawer panel with backdrop, animations, and accessibility features.\n * Consolidates common patterns from all drawer implementations.\n */\n\nimport { useEffect, useRef, useCallback, useState } from \"react\";\nimport \"./Drawer.css\";\n\nexport interface DrawerProps {\n /** Whether the drawer is open */\n isOpen: boolean;\n /** Callback when drawer should close */\n onClose: () => void;\n /** Drawer title */\n title: React.ReactNode;\n /** Optional subtitle */\n subtitle?: React.ReactNode;\n /** Drawer width preset or custom CSS width value */\n width?: \"narrow\" | \"medium\" | \"wide\" | (string & Record<never, never>);\n /** Drawer content */\n children: React.ReactNode;\n /** Optional footer content */\n footer?: React.ReactNode;\n /** Additional CSS class for drawer panel */\n className?: string;\n /** ARIA labelledby ID (defaults to internal title ID) */\n ariaLabelledBy?: string;\n /** ARIA describedby ID (defaults to internal subtitle ID if subtitle exists) */\n ariaDescribedBy?: string;\n /** Accessible label for the close button. Default: \"Close\" */\n closeLabel?: string;\n}\n\nconst WIDTH_PRESETS = {\n narrow: \"400px\",\n medium: \"520px\",\n wide: \"900px\",\n};\n\nexport function Drawer({\n isOpen,\n onClose,\n title,\n subtitle,\n width = \"medium\",\n children,\n footer,\n className = \"\",\n ariaLabelledBy,\n ariaDescribedBy,\n closeLabel = \"Close\",\n}: DrawerProps) {\n const drawerRef = useRef<HTMLDivElement>(null);\n const previousActiveElementRef = useRef<HTMLElement | null>(null);\n const closeButtonRef = useRef<HTMLButtonElement>(null);\n\n // Track if this is the first render to enable animation on initial open\n // Without this, when the component mounts with isOpen=true, there's no\n // transition because CSS transitions only apply to state changes, not initial renders.\n const [shouldAnimate, setShouldAnimate] = useState(false);\n const hasInitialized = useRef(false);\n\n useEffect(() => {\n if (isOpen && !hasInitialized.current) {\n // First time opening - delay adding open class to trigger animation\n hasInitialized.current = true;\n // Use double rAF to ensure the closed state is painted first\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n setShouldAnimate(true);\n });\n });\n } else if (isOpen) {\n setShouldAnimate(true);\n } else {\n setShouldAnimate(false);\n }\n }, [isOpen]);\n\n // Compute the actual open state for CSS classes\n const isVisuallyOpen = isOpen && shouldAnimate;\n\n // Compute width value\n const widthValue =\n width in WIDTH_PRESETS ? WIDTH_PRESETS[width as keyof typeof WIDTH_PRESETS] : width;\n\n // Internal IDs for ARIA\n const titleId = ariaLabelledBy || \"drawer-title\";\n const subtitleId = ariaDescribedBy || (subtitle ? \"drawer-subtitle\" : undefined);\n\n // Focus management and trap\n useEffect(() => {\n if (!isOpen) return;\n\n // Store previously focused element\n previousActiveElementRef.current = document.activeElement as HTMLElement;\n\n const drawer = drawerRef.current;\n if (!drawer) return;\n\n // Focus close button when drawer opens\n requestAnimationFrame(() => {\n closeButtonRef.current?.focus();\n });\n\n // Setup focus trap\n const focusableElements = drawer.querySelectorAll<HTMLElement>(\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])',\n );\n\n const firstFocusable = focusableElements[0];\n const lastFocusable = focusableElements[focusableElements.length - 1];\n\n const handleKeyDown = (e: KeyboardEvent) => {\n // Escape to close\n if (e.key === \"Escape\") {\n onClose();\n return;\n }\n\n // Tab key focus trap\n if (e.key === \"Tab\") {\n if (e.shiftKey) {\n // Shift + Tab\n if (document.activeElement === firstFocusable) {\n e.preventDefault();\n lastFocusable?.focus();\n }\n } else {\n // Tab\n if (document.activeElement === lastFocusable) {\n e.preventDefault();\n firstFocusable?.focus();\n }\n }\n }\n };\n\n drawer.addEventListener(\"keydown\", handleKeyDown);\n\n return () => {\n drawer.removeEventListener(\"keydown\", handleKeyDown);\n // Restore focus when drawer closes\n if (previousActiveElementRef.current) {\n previousActiveElementRef.current.focus();\n }\n };\n }, [isOpen, onClose]);\n\n // Handle backdrop click\n const handleBackdropClick = useCallback(\n (e: React.MouseEvent) => {\n if (e.target === e.currentTarget) {\n onClose();\n }\n },\n [onClose],\n );\n\n return (\n <div\n className={`drawer__backdrop ${isVisuallyOpen ? \"drawer__backdrop--open\" : \"\"}`}\n onClick={handleBackdropClick}\n role=\"presentation\"\n aria-hidden={!isOpen}\n >\n <aside\n ref={drawerRef}\n className={`drawer ${isVisuallyOpen ? \"drawer--open\" : \"\"} ${className}`}\n style={{ width: widthValue, maxWidth: \"100vw\" }}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={titleId}\n aria-describedby={subtitleId}\n >\n {/* Header */}\n <header className=\"drawer__header\">\n <div className=\"drawer__title-row\">\n <h2 id={titleId} className=\"drawer__title\">\n {title}\n </h2>\n <button\n ref={closeButtonRef}\n type=\"button\"\n className=\"drawer__close-btn\"\n onClick={onClose}\n aria-label={closeLabel}\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M18 6L6 18\" />\n <path d=\"M6 6l12 12\" />\n </svg>\n </button>\n </div>\n {subtitle && (\n <p id={subtitleId} className=\"drawer__subtitle\">\n {subtitle}\n </p>\n )}\n </header>\n\n {/* Content */}\n <div className=\"drawer__content\">{children}</div>\n\n {/* Footer */}\n {footer && <footer className=\"drawer__footer\">{footer}</footer>}\n </aside>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Drawer2.js","names":[],"sources":["../../../src/components/Drawer/Drawer.tsx"],"sourcesContent":["/**\n * Drawer Component\n *\n * A reusable right-side drawer panel with backdrop, animations, and accessibility features.\n * Consolidates common patterns from all drawer implementations.\n */\n\nimport { useEffect, useRef, useCallback, useState } from \"react\";\nimport \"./Drawer.css\";\n\nexport interface DrawerProps {\n /** Whether the drawer is open */\n isOpen: boolean;\n /** Callback when drawer should close */\n onClose: () => void;\n /** Drawer title */\n title: React.ReactNode;\n /** Optional subtitle */\n subtitle?: React.ReactNode;\n /** Drawer width preset or custom CSS width value */\n width?: \"narrow\" | \"medium\" | \"wide\" | (string & Record<never, never>);\n /** Drawer content */\n children: React.ReactNode;\n /** Optional footer content */\n footer?: React.ReactNode;\n /** Additional CSS class for drawer panel */\n className?: string;\n /** ARIA labelledby ID (defaults to internal title ID) */\n ariaLabelledBy?: string;\n /** ARIA describedby ID (defaults to internal subtitle ID if subtitle exists) */\n ariaDescribedBy?: string;\n /** Accessible label for the close button. Default: \"Close\" */\n closeLabel?: string;\n /**\n * Refuse dismissal by Escape or a backdrop click, shaking the panel instead.\n * The close button and any footer control still call `onClose` directly, so\n * route those through the same guard when they also need confirming.\n */\n preventDismiss?: boolean;\n /**\n * Called when `preventDismiss` blocked a dismissal, so the consumer can offer\n * an explicit way out (a discard confirmation, for instance).\n */\n onDismissAttempt?: () => void;\n}\n\nconst WIDTH_PRESETS = {\n narrow: \"400px\",\n medium: \"520px\",\n wide: \"900px\",\n};\n\nexport function Drawer({\n isOpen,\n onClose,\n title,\n subtitle,\n width = \"medium\",\n children,\n footer,\n className = \"\",\n ariaLabelledBy,\n ariaDescribedBy,\n closeLabel = \"Close\",\n preventDismiss = false,\n onDismissAttempt,\n}: DrawerProps) {\n const drawerRef = useRef<HTMLDivElement>(null);\n\n const [isShaking, setIsShaking] = useState(false);\n\n // Read the guard through refs so `blockDismiss` keeps a stable identity. The\n // focus-management effect depends on it, and if the identity changed the\n // moment the consumer marks the drawer dirty, that effect would re-run on the\n // first keystroke and steal focus from the field being typed into.\n const preventDismissRef = useRef(preventDismiss);\n preventDismissRef.current = preventDismiss;\n const onDismissAttemptRef = useRef(onDismissAttempt);\n onDismissAttemptRef.current = onDismissAttempt;\n\n /** Blocks a dismissal when the guard is on. Returns whether it blocked. */\n const blockDismiss = useCallback(() => {\n if (!preventDismissRef.current) return false;\n setIsShaking(true);\n onDismissAttemptRef.current?.();\n return true;\n }, []);\n\n // Guarded on the animation name so an unrelated animation on the panel does\n // not clear the flag early.\n const handleAnimationEnd = useCallback((e: React.AnimationEvent) => {\n if (e.animationName === \"drawer-shake\") setIsShaking(false);\n }, []);\n const previousActiveElementRef = useRef<HTMLElement | null>(null);\n const closeButtonRef = useRef<HTMLButtonElement>(null);\n\n // Track if this is the first render to enable animation on initial open\n // Without this, when the component mounts with isOpen=true, there's no\n // transition because CSS transitions only apply to state changes, not initial renders.\n const [shouldAnimate, setShouldAnimate] = useState(false);\n const hasInitialized = useRef(false);\n\n useEffect(() => {\n if (isOpen && !hasInitialized.current) {\n // First time opening - delay adding open class to trigger animation\n hasInitialized.current = true;\n // Use double rAF to ensure the closed state is painted first\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n setShouldAnimate(true);\n });\n });\n } else if (isOpen) {\n setShouldAnimate(true);\n } else {\n setShouldAnimate(false);\n }\n }, [isOpen]);\n\n // Compute the actual open state for CSS classes\n const isVisuallyOpen = isOpen && shouldAnimate;\n\n // Compute width value\n const widthValue =\n width in WIDTH_PRESETS ? WIDTH_PRESETS[width as keyof typeof WIDTH_PRESETS] : width;\n\n // Internal IDs for ARIA\n const titleId = ariaLabelledBy || \"drawer-title\";\n const subtitleId = ariaDescribedBy || (subtitle ? \"drawer-subtitle\" : undefined);\n\n // Focus management and trap\n useEffect(() => {\n if (!isOpen) return;\n\n // Store previously focused element\n previousActiveElementRef.current = document.activeElement as HTMLElement;\n\n const drawer = drawerRef.current;\n if (!drawer) return;\n\n // Focus close button when drawer opens\n requestAnimationFrame(() => {\n closeButtonRef.current?.focus();\n });\n\n // Setup focus trap\n const focusableElements = drawer.querySelectorAll<HTMLElement>(\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])',\n );\n\n const firstFocusable = focusableElements[0];\n const lastFocusable = focusableElements[focusableElements.length - 1];\n\n const handleKeyDown = (e: KeyboardEvent) => {\n // Escape to close, unless the guard blocks it\n if (e.key === \"Escape\") {\n if (blockDismiss()) return;\n onClose();\n return;\n }\n\n // Tab key focus trap\n if (e.key === \"Tab\") {\n if (e.shiftKey) {\n // Shift + Tab\n if (document.activeElement === firstFocusable) {\n e.preventDefault();\n lastFocusable?.focus();\n }\n } else {\n // Tab\n if (document.activeElement === lastFocusable) {\n e.preventDefault();\n firstFocusable?.focus();\n }\n }\n }\n };\n\n drawer.addEventListener(\"keydown\", handleKeyDown);\n\n return () => {\n drawer.removeEventListener(\"keydown\", handleKeyDown);\n // Restore focus when drawer closes\n if (previousActiveElementRef.current) {\n previousActiveElementRef.current.focus();\n }\n };\n }, [isOpen, onClose, blockDismiss]);\n\n // Handle backdrop click\n const handleBackdropClick = useCallback(\n (e: React.MouseEvent) => {\n if (e.target === e.currentTarget) {\n if (blockDismiss()) return;\n onClose();\n }\n },\n [onClose, blockDismiss],\n );\n\n return (\n <div\n className={`drawer__backdrop ${isVisuallyOpen ? \"drawer__backdrop--open\" : \"\"}`}\n onClick={handleBackdropClick}\n role=\"presentation\"\n aria-hidden={!isOpen}\n // `aria-hidden` takes the closed drawer out of the accessibility tree but\n // leaves its focusable children in the tab sequence, so a keyboard user\n // tabs into an invisible form. `inert` removes both.\n inert={!isOpen}\n >\n <aside\n ref={drawerRef}\n className={`drawer ${isVisuallyOpen ? \"drawer--open\" : \"\"} ${\n isShaking ? \"drawer--shaking\" : \"\"\n } ${className}`}\n style={{ width: widthValue, maxWidth: \"100vw\" }}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={titleId}\n aria-describedby={subtitleId}\n onAnimationEnd={handleAnimationEnd}\n >\n {/* Header */}\n <header className=\"drawer__header\">\n <div className=\"drawer__title-row\">\n <h2 id={titleId} className=\"drawer__title\">\n {title}\n </h2>\n <button\n ref={closeButtonRef}\n type=\"button\"\n className=\"drawer__close-btn\"\n onClick={onClose}\n aria-label={closeLabel}\n >\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M18 6L6 18\" />\n <path d=\"M6 6l12 12\" />\n </svg>\n </button>\n </div>\n {subtitle && (\n <p id={subtitleId} className=\"drawer__subtitle\">\n {subtitle}\n </p>\n )}\n </header>\n\n {/* Content */}\n <div className=\"drawer__content\">{children}</div>\n\n {/* Footer */}\n {footer && <footer className=\"drawer__footer\">{footer}</footer>}\n </aside>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -8,6 +8,19 @@ export interface PageHeaderProps {
|
|
|
8
8
|
actions?: ReactNode;
|
|
9
9
|
/** Error message to display below the header */
|
|
10
10
|
error?: string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Secondary line under the message, for the raw detail a server returned.
|
|
13
|
+
* Kept separate so the message stays readable when the detail is long.
|
|
14
|
+
*/
|
|
15
|
+
errorDetail?: string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Callback for the Retry button, rendered before the dismiss control when
|
|
18
|
+
* set. Dismissal is a pure dismissal and must not retry, which is why these
|
|
19
|
+
* are two props rather than one.
|
|
20
|
+
*/
|
|
21
|
+
onRetry?: () => void;
|
|
22
|
+
/** Label for the Retry button. Default: "Retry" */
|
|
23
|
+
retryLabel?: string;
|
|
11
24
|
/** Callback when error is dismissed */
|
|
12
25
|
onErrorDismiss?: () => void;
|
|
13
26
|
/** Additional CSS class names */
|
|
@@ -15,4 +28,4 @@ export interface PageHeaderProps {
|
|
|
15
28
|
/** Accessible label for the error-dismiss button. Default: "Dismiss error" */
|
|
16
29
|
dismissErrorLabel?: string;
|
|
17
30
|
}
|
|
18
|
-
export declare function PageHeader({ title, description, actions, error, onErrorDismiss, className, dismissErrorLabel, }: PageHeaderProps): import("react").JSX.Element;
|
|
31
|
+
export declare function PageHeader({ title, description, actions, error, onErrorDismiss, className, errorDetail, onRetry, retryLabel, dismissErrorLabel, }: PageHeaderProps): import("react").JSX.Element;
|
|
@@ -1,43 +1,59 @@
|
|
|
1
|
+
import { Button as e } from "../Button/Button2.js";
|
|
1
2
|
/* empty css */
|
|
2
|
-
import { jsx as
|
|
3
|
+
import { jsx as t, jsxs as n } from "react/jsx-runtime";
|
|
3
4
|
//#region src/components/PageHeader/PageHeader.tsx
|
|
4
|
-
function
|
|
5
|
-
let
|
|
6
|
-
return /* @__PURE__ */
|
|
7
|
-
className:
|
|
8
|
-
children: [/* @__PURE__ */
|
|
5
|
+
function r({ title: r, description: i, actions: a, error: o, onErrorDismiss: s, className: c = "", errorDetail: l, onRetry: u, retryLabel: d = "Retry", dismissErrorLabel: f = "Dismiss error" }) {
|
|
6
|
+
let p = ["page-header", c].filter(Boolean).join(" ");
|
|
7
|
+
return /* @__PURE__ */ n("header", {
|
|
8
|
+
className: p,
|
|
9
|
+
children: [/* @__PURE__ */ n("div", {
|
|
9
10
|
className: "page-header__content",
|
|
10
|
-
children: [/* @__PURE__ */
|
|
11
|
+
children: [/* @__PURE__ */ n("div", {
|
|
11
12
|
className: "page-header__text",
|
|
12
|
-
children: [/* @__PURE__ */
|
|
13
|
+
children: [/* @__PURE__ */ t("h1", {
|
|
13
14
|
className: "page-header__title",
|
|
14
|
-
children: n
|
|
15
|
-
}), r && /* @__PURE__ */ e("p", {
|
|
16
|
-
className: "page-header__description",
|
|
17
15
|
children: r
|
|
16
|
+
}), i && /* @__PURE__ */ t("p", {
|
|
17
|
+
className: "page-header__description",
|
|
18
|
+
children: i
|
|
18
19
|
})]
|
|
19
|
-
}),
|
|
20
|
+
}), a && /* @__PURE__ */ t("div", {
|
|
20
21
|
className: "page-header__actions",
|
|
21
|
-
children:
|
|
22
|
+
children: a
|
|
22
23
|
})]
|
|
23
|
-
}),
|
|
24
|
+
}), o && /* @__PURE__ */ n("div", {
|
|
24
25
|
className: "page-header__error",
|
|
25
26
|
role: "alert",
|
|
26
|
-
children: [/* @__PURE__ */
|
|
27
|
-
className: "page-header__error-
|
|
28
|
-
children:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
children: [/* @__PURE__ */ n("div", {
|
|
28
|
+
className: "page-header__error-body",
|
|
29
|
+
children: [/* @__PURE__ */ t("span", {
|
|
30
|
+
className: "page-header__error-text",
|
|
31
|
+
children: o
|
|
32
|
+
}), l && /* @__PURE__ */ t("span", {
|
|
33
|
+
className: "page-header__error-detail",
|
|
34
|
+
children: l
|
|
35
|
+
})]
|
|
36
|
+
}), (u || s) && /* @__PURE__ */ n("div", {
|
|
37
|
+
className: "page-header__error-actions",
|
|
38
|
+
children: [u && /* @__PURE__ */ t(e, {
|
|
39
|
+
variant: "secondary",
|
|
40
|
+
size: "small",
|
|
41
|
+
className: "page-header__error-retry",
|
|
42
|
+
onClick: u,
|
|
43
|
+
children: d
|
|
44
|
+
}), s && /* @__PURE__ */ t("button", {
|
|
45
|
+
type: "button",
|
|
46
|
+
className: "page-header__error-dismiss",
|
|
47
|
+
onClick: s,
|
|
48
|
+
"aria-label": f,
|
|
49
|
+
children: "×"
|
|
50
|
+
})]
|
|
35
51
|
})]
|
|
36
52
|
})]
|
|
37
53
|
});
|
|
38
54
|
}
|
|
39
55
|
//#endregion
|
|
40
|
-
export {
|
|
56
|
+
export { r as PageHeader };
|
|
41
57
|
|
|
42
58
|
//# sourceMappingURL=PageHeader2.js.map
|
|
43
59
|
import "../../assets/components/PageHeader/PageHeader.css";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PageHeader2.js","names":[],"sources":["../../../src/components/PageHeader/PageHeader.tsx"],"sourcesContent":["/**\n * PageHeader Component\n *\n * Standardized page header with title, description, and optional action buttons.\n * Provides consistent styling and layout across all pages.\n *\n * @example\n * // Basic usage\n * <PageHeader\n * title=\"Models\"\n * description=\"Manage and browse your local models\"\n * />\n *\n * @example\n * // With action buttons\n * <PageHeader\n * title=\"Settings\"\n * description=\"Configure application settings\"\n * actions={\n * <>\n * <Button variant=\"secondary\">Import</Button>\n * <Button variant=\"primary\">Save</Button>\n * </>\n * }\n * />\n *\n * @example\n * // With error display\n * <PageHeader\n * title=\"Engines\"\n * description=\"Manage runtime engines\"\n * error={error}\n * onErrorDismiss={() => clearError()}\n * />\n */\n\nimport type { ReactNode } from \"react\";\nimport \"./PageHeader.css\";\n\nexport interface PageHeaderProps {\n /** Page title - displayed as h1 */\n title: string;\n /** Page description - displayed below title */\n description?: string;\n /** Optional action buttons or elements on the right side */\n actions?: ReactNode;\n /** Error message to display below the header */\n error?: string | null;\n /** Callback when error is dismissed */\n onErrorDismiss?: () => void;\n /** Additional CSS class names */\n className?: string;\n /** Accessible label for the error-dismiss button. Default: \"Dismiss error\" */\n dismissErrorLabel?: string;\n}\n\nexport function PageHeader({\n title,\n description,\n actions,\n error,\n onErrorDismiss,\n className = \"\",\n dismissErrorLabel = \"Dismiss error\",\n}: PageHeaderProps) {\n const classes = [\"page-header\", className].filter(Boolean).join(\" \");\n\n return (\n <header className={classes}>\n <div className=\"page-header__content\">\n <div className=\"page-header__text\">\n <h1 className=\"page-header__title\">{title}</h1>\n {description && <p className=\"page-header__description\">{description}</p>}\n </div>\n {actions && <div className=\"page-header__actions\">{actions}</div>}\n </div>\n {error && (\n <div className=\"page-header__error\" role=\"alert\">\n <span className=\"page-header__error-text\">{error}</span>\n {onErrorDismiss && (\n <button\n
|
|
1
|
+
{"version":3,"file":"PageHeader2.js","names":[],"sources":["../../../src/components/PageHeader/PageHeader.tsx"],"sourcesContent":["/**\n * PageHeader Component\n *\n * Standardized page header with title, description, and optional action buttons.\n * Provides consistent styling and layout across all pages.\n *\n * @example\n * // Basic usage\n * <PageHeader\n * title=\"Models\"\n * description=\"Manage and browse your local models\"\n * />\n *\n * @example\n * // With action buttons\n * <PageHeader\n * title=\"Settings\"\n * description=\"Configure application settings\"\n * actions={\n * <>\n * <Button variant=\"secondary\">Import</Button>\n * <Button variant=\"primary\">Save</Button>\n * </>\n * }\n * />\n *\n * @example\n * // With error display\n * <PageHeader\n * title=\"Engines\"\n * description=\"Manage runtime engines\"\n * error={error}\n * onErrorDismiss={() => clearError()}\n * />\n */\n\nimport type { ReactNode } from \"react\";\nimport { Button } from \"../Button\";\nimport \"./PageHeader.css\";\n\nexport interface PageHeaderProps {\n /** Page title - displayed as h1 */\n title: string;\n /** Page description - displayed below title */\n description?: string;\n /** Optional action buttons or elements on the right side */\n actions?: ReactNode;\n /** Error message to display below the header */\n error?: string | null;\n /**\n * Secondary line under the message, for the raw detail a server returned.\n * Kept separate so the message stays readable when the detail is long.\n */\n errorDetail?: string | null;\n /**\n * Callback for the Retry button, rendered before the dismiss control when\n * set. Dismissal is a pure dismissal and must not retry, which is why these\n * are two props rather than one.\n */\n onRetry?: () => void;\n /** Label for the Retry button. Default: \"Retry\" */\n retryLabel?: string;\n /** Callback when error is dismissed */\n onErrorDismiss?: () => void;\n /** Additional CSS class names */\n className?: string;\n /** Accessible label for the error-dismiss button. Default: \"Dismiss error\" */\n dismissErrorLabel?: string;\n}\n\nexport function PageHeader({\n title,\n description,\n actions,\n error,\n onErrorDismiss,\n className = \"\",\n errorDetail,\n onRetry,\n retryLabel = \"Retry\",\n dismissErrorLabel = \"Dismiss error\",\n}: PageHeaderProps) {\n const classes = [\"page-header\", className].filter(Boolean).join(\" \");\n\n return (\n <header className={classes}>\n <div className=\"page-header__content\">\n <div className=\"page-header__text\">\n <h1 className=\"page-header__title\">{title}</h1>\n {description && <p className=\"page-header__description\">{description}</p>}\n </div>\n {actions && <div className=\"page-header__actions\">{actions}</div>}\n </div>\n {error && (\n <div className=\"page-header__error\" role=\"alert\">\n <div className=\"page-header__error-body\">\n <span className=\"page-header__error-text\">{error}</span>\n {errorDetail && (\n <span className=\"page-header__error-detail\">{errorDetail}</span>\n )}\n </div>\n {(onRetry || onErrorDismiss) && (\n <div className=\"page-header__error-actions\">\n {onRetry && (\n <Button\n variant=\"secondary\"\n size=\"small\"\n className=\"page-header__error-retry\"\n onClick={onRetry}\n >\n {retryLabel}\n </Button>\n )}\n {onErrorDismiss && (\n <button\n type=\"button\"\n className=\"page-header__error-dismiss\"\n onClick={onErrorDismiss}\n aria-label={dismissErrorLabel}\n >\n ×\n </button>\n )}\n </div>\n )}\n </div>\n )}\n </header>\n );\n}\n"],"mappings":";;;;AAsEA,SAAgB,EAAW,EACzB,UACA,gBACA,YACA,UACA,mBACA,eAAY,IACZ,gBACA,YACA,gBAAa,SACb,uBAAoB,mBACF;CAClB,IAAM,IAAU,CAAC,eAAe,CAAS,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG;CAEnE,OACE,kBAAC,UAAD;EAAQ,WAAW;EAAnB,UAAA,CACE,kBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;KAAsB,UAAA;IAAU,CAAA,GAC7C,KAAe,kBAAC,KAAD;KAAG,WAAU;KAA4B,UAAA;IAAe,CAAA,CACrE;GACJ,CAAA,GAAA,KAAW,kBAAC,OAAD;IAAK,WAAU;IAAwB,UAAA;GAAa,CAAA,CAC7D;EACJ,CAAA,GAAA,KACC,kBAAC,OAAD;GAAK,WAAU;GAAqB,MAAK;GAAzC,UAAA,CACE,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACE,kBAAC,QAAD;KAAM,WAAU;KAA2B,UAAA;IAAY,CAAA,GACtD,KACC,kBAAC,QAAD;KAAM,WAAU;KAA6B,UAAA;IAAkB,CAAA,CAE9D;GACH,CAAA,IAAA,KAAW,MACX,kBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACG,KACC,kBAAC,GAAD;KACE,SAAQ;KACR,MAAK;KACL,WAAU;KACV,SAAS;KAER,UAAA;IACK,CAAA,GAET,KACC,kBAAC,UAAD;KACE,MAAK;KACL,WAAU;KACV,SAAS;KACT,cAAY;KACb,UAAA;IAEO,CAAA,CAEP;GAEJ,CAAA,CAAA;EAED,CAAA,CAAA;;AAEZ"}
|
package/package.json
CHANGED