@astralui/core 0.1.10 → 0.1.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/index.cjs CHANGED
@@ -173,48 +173,139 @@ function AstralThemeProvider({ colors, children }) {
173
173
  children
174
174
  ] });
175
175
  }
176
- function useDismiss(opened, onClose) {
176
+ var dialogSeq = 0;
177
+ var dialogStack = [];
178
+ var FOCUSABLE = [
179
+ "a[href]",
180
+ "area[href]",
181
+ "button:not([disabled])",
182
+ 'input:not([disabled]):not([type="hidden"])',
183
+ "select:not([disabled])",
184
+ "textarea:not([disabled])",
185
+ "iframe",
186
+ "object",
187
+ "embed",
188
+ "[contenteditable]",
189
+ '[tabindex]:not([tabindex="-1"])'
190
+ ].join(",");
191
+ function useDialog(opened, onClose) {
192
+ const panelRef = react.useRef(null);
193
+ const idRef = react.useRef("");
194
+ if (!idRef.current) idRef.current = "au-dialog-title-" + String(++dialogSeq);
195
+ const onCloseRef = react.useRef(onClose);
196
+ onCloseRef.current = onClose;
177
197
  react.useEffect(() => {
178
198
  if (!opened) return;
199
+ const panel = panelRef.current;
200
+ if (!panel) return;
201
+ const restoreTo = document.activeElement;
202
+ dialogStack.push(panel);
203
+ const isTop = () => dialogStack[dialogStack.length - 1] === panel;
204
+ const focusables = () => Array.from(panel.querySelectorAll(FOCUSABLE)).filter(
205
+ (el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement
206
+ );
207
+ const raf = requestAnimationFrame(() => {
208
+ if (panel.contains(document.activeElement)) return;
209
+ const list = focusables();
210
+ const target = list.find((el) => !el.classList.contains("au-modal-x")) ?? list[0] ?? panel;
211
+ target.focus();
212
+ });
179
213
  const onKey = (e) => {
180
- if (e.key === "Escape") onClose();
214
+ if (!isTop()) return;
215
+ if (e.key === "Escape") {
216
+ onCloseRef.current();
217
+ return;
218
+ }
219
+ if (e.key !== "Tab") return;
220
+ const list = focusables();
221
+ if (!list.length) {
222
+ e.preventDefault();
223
+ panel.focus();
224
+ return;
225
+ }
226
+ const first = list[0];
227
+ const last = list[list.length - 1];
228
+ const active = document.activeElement;
229
+ if (!panel.contains(active)) {
230
+ e.preventDefault();
231
+ (e.shiftKey ? last : first).focus();
232
+ return;
233
+ }
234
+ if (e.shiftKey && active === first) {
235
+ e.preventDefault();
236
+ last.focus();
237
+ } else if (!e.shiftKey && active === last) {
238
+ e.preventDefault();
239
+ first.focus();
240
+ }
181
241
  };
182
- document.addEventListener("keydown", onKey);
183
- const prev = document.body.style.overflow;
242
+ document.addEventListener("keydown", onKey, true);
243
+ const prevOverflow = document.body.style.overflow;
184
244
  document.body.style.overflow = "hidden";
185
245
  return () => {
186
- document.removeEventListener("keydown", onKey);
187
- document.body.style.overflow = prev;
246
+ cancelAnimationFrame(raf);
247
+ document.removeEventListener("keydown", onKey, true);
248
+ const at = dialogStack.indexOf(panel);
249
+ if (at !== -1) dialogStack.splice(at, 1);
250
+ document.body.style.overflow = prevOverflow;
251
+ if (restoreTo && document.contains(restoreTo)) restoreTo.focus();
188
252
  };
189
- }, [opened, onClose]);
253
+ }, [opened]);
254
+ return { panelRef, titleId: idRef.current };
190
255
  }
191
256
  function AstralModal({ opened, onClose, title, brand, width = 460, zIndex, children }) {
192
- useDismiss(opened, onClose);
257
+ const { panelRef, titleId } = useDialog(opened, onClose);
193
258
  if (!opened) return null;
194
259
  const style = { maxWidth: width, ...brand ? { "--au-ic": brand } : {} };
195
260
  return reactDom.createPortal(
196
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-modal-overlay", onMouseDown: onClose, style: zIndex != null ? { zIndex } : void 0, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "au-modal-panel", style, onMouseDown: (e) => e.stopPropagation(), children: [
197
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "au-modal-head", children: [
198
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "au-modal-title", children: title }),
199
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "au-modal-x", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconX, {}) })
200
- ] }),
201
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-modal-body", children })
202
- ] }) }),
261
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-modal-overlay", onMouseDown: onClose, style: zIndex != null ? { zIndex } : void 0, children: /* @__PURE__ */ jsxRuntime.jsxs(
262
+ "div",
263
+ {
264
+ ref: panelRef,
265
+ className: "au-modal-panel",
266
+ style,
267
+ onMouseDown: (e) => e.stopPropagation(),
268
+ role: "dialog",
269
+ "aria-modal": "true",
270
+ "aria-labelledby": title ? titleId : void 0,
271
+ tabIndex: -1,
272
+ children: [
273
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "au-modal-head", children: [
274
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "au-modal-title", id: titleId, children: title }),
275
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "au-modal-x", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconX, {}) })
276
+ ] }),
277
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-modal-body", children })
278
+ ]
279
+ }
280
+ ) }),
203
281
  document.body
204
282
  );
205
283
  }
206
284
  function AstralDrawer({ opened, onClose, title, brand, width = 440, zIndex, children }) {
207
- useDismiss(opened, onClose);
285
+ const { panelRef, titleId } = useDialog(opened, onClose);
208
286
  if (!opened) return null;
209
287
  const style = { width, ...brand ? { "--au-ic": brand } : {} };
210
288
  return reactDom.createPortal(
211
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-drawer-overlay", onMouseDown: onClose, style: zIndex != null ? { zIndex } : void 0, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "au-drawer-panel", style, onMouseDown: (e) => e.stopPropagation(), children: [
212
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "au-drawer-head", children: [
213
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "au-drawer-title", children: title }),
214
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "au-modal-x", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconX, {}) })
215
- ] }),
216
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-drawer-body", children })
217
- ] }) }),
289
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-drawer-overlay", onMouseDown: onClose, style: zIndex != null ? { zIndex } : void 0, children: /* @__PURE__ */ jsxRuntime.jsxs(
290
+ "div",
291
+ {
292
+ ref: panelRef,
293
+ className: "au-drawer-panel",
294
+ style,
295
+ onMouseDown: (e) => e.stopPropagation(),
296
+ role: "dialog",
297
+ "aria-modal": "true",
298
+ "aria-labelledby": title ? titleId : void 0,
299
+ tabIndex: -1,
300
+ children: [
301
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "au-drawer-head", children: [
302
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "au-drawer-title", id: titleId, children: title }),
303
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "au-modal-x", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconX, {}) })
304
+ ] }),
305
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "au-drawer-body", children })
306
+ ]
307
+ }
308
+ ) }),
218
309
  document.body
219
310
  );
220
311
  }
@@ -918,6 +1009,17 @@ var COLOR_MAP = {
918
1009
  replied: "#8b5cf6",
919
1010
  success: "#16a34a"
920
1011
  };
1012
+ var FG = {
1013
+ "#eab308": ["#854d0e", "#fde047"],
1014
+ "#06b6d4": ["#155e75", "#67e8f9"],
1015
+ "#EF8E2E": ["#9a4a06", "#fdba74"],
1016
+ "#9ca3af": ["#4b5563", "#d1d5db"],
1017
+ "#16a34a": ["#14532d", "#4ade80"],
1018
+ "#3b82f6": ["#1e40af", "#93c5fd"],
1019
+ "#ef4444": ["#991b1b", "#fca5a5"],
1020
+ "#8b5cf6": ["#5b21b6", "#c4b5fd"]
1021
+ };
1022
+ var SOLID = { "#16a34a": "#166534" };
921
1023
  var FILLED = /* @__PURE__ */ new Set(["running", "active", "online"]);
922
1024
  function StatusBadge({ status, label, color, filled }) {
923
1025
  const c = color ?? COLOR_MAP[status] ?? "#9ca3af";
@@ -927,7 +1029,11 @@ function StatusBadge({ status, label, color, filled }) {
927
1029
  "span",
928
1030
  {
929
1031
  className: `au-sbadge${isFilled ? " filled" : ""}`,
930
- style: { "--au-sb": c },
1032
+ style: {
1033
+ "--au-sb": c,
1034
+ ...FG[c] ? { "--au-sb-fg": `light-dark(${FG[c][0]}, ${FG[c][1]})` } : {},
1035
+ ...SOLID[c] ? { "--au-sb-solid": SOLID[c] } : {}
1036
+ },
931
1037
  title: typeof text === "string" ? text : void 0,
932
1038
  children: [
933
1039
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "au-sbadge-dot" }),