@mshuja288/aksuicore 0.2.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.
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ import { useEffect, useSyncExternalStore } from "react";
4
+ import { createPortal } from "react-dom";
5
+
6
+ function useIsClient() {
7
+ return useSyncExternalStore(
8
+ () => () => {},
9
+ () => true,
10
+ () => false,
11
+ );
12
+ }
13
+
14
+ function SuccessIcon() {
15
+ return (
16
+ <svg className="snackbar-icon" viewBox="0 0 24 24" aria-hidden="true">
17
+ <circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" strokeWidth="1.8" />
18
+ <path
19
+ d="M8.2 12.2 10.8 14.8 15.8 9.6"
20
+ fill="none"
21
+ stroke="currentColor"
22
+ strokeWidth="1.8"
23
+ strokeLinecap="round"
24
+ strokeLinejoin="round"
25
+ />
26
+ </svg>
27
+ );
28
+ }
29
+
30
+ function AlertIcon() {
31
+ return (
32
+ <svg className="snackbar-icon" viewBox="0 0 24 24" aria-hidden="true">
33
+ <path
34
+ d="M12 3.8 3.8 19.2h16.4L12 3.8Z"
35
+ fill="none"
36
+ stroke="currentColor"
37
+ strokeWidth="1.8"
38
+ strokeLinejoin="round"
39
+ />
40
+ <path d="M12 10v4.2" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
41
+ <circle cx="12" cy="16.8" r="1" fill="currentColor" />
42
+ </svg>
43
+ );
44
+ }
45
+
46
+ export function Snackbar({
47
+ message,
48
+ success,
49
+ onDismiss,
50
+ }: {
51
+ message: string;
52
+ success: boolean;
53
+ onDismiss?: () => void;
54
+ }) {
55
+ const isClient = useIsClient();
56
+
57
+ useEffect(() => {
58
+ if (!message || !onDismiss) {
59
+ return;
60
+ }
61
+
62
+ const timeout = window.setTimeout(onDismiss, success ? 3200 : 4800);
63
+ return () => window.clearTimeout(timeout);
64
+ }, [message, success, onDismiss]);
65
+
66
+ if (!isClient || !message) {
67
+ return null;
68
+ }
69
+
70
+ return createPortal(
71
+ <div className="snackbar-host" aria-live="polite">
72
+ <div className={success ? "snackbar success" : "snackbar"} role="status">
73
+ {success ? <SuccessIcon /> : <AlertIcon />}
74
+ <p>{message}</p>
75
+ {onDismiss ? (
76
+ <button type="button" className="snackbar-close" aria-label="Dismiss" onClick={onDismiss}>
77
+ ×
78
+ </button>
79
+ ) : null}
80
+ </div>
81
+ </div>,
82
+ document.body,
83
+ );
84
+ }
85
+
86
+ /** @deprecated Prefer Snackbar — alias kept for configurator migration */
87
+ export const StatusMessage = Snackbar;
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState, type ReactNode } from "react";
4
+ import { Button } from "./Button";
5
+
6
+ export function Topbar({
7
+ title,
8
+ logoSrc,
9
+ logoAlt = "Logo",
10
+ username = "",
11
+ onLogout,
12
+ children,
13
+ }: {
14
+ title: string;
15
+ logoSrc?: string;
16
+ logoAlt?: string;
17
+ username?: string;
18
+ onLogout?: () => void;
19
+ children?: ReactNode;
20
+ }) {
21
+ const [menuOpen, setMenuOpen] = useState(false);
22
+ const menuRef = useRef<HTMLDivElement>(null);
23
+ const initial = (username || "U").slice(0, 1).toUpperCase();
24
+
25
+ useEffect(() => {
26
+ if (!menuOpen) {
27
+ return;
28
+ }
29
+
30
+ function onPointerDown(event: MouseEvent) {
31
+ if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
32
+ setMenuOpen(false);
33
+ }
34
+ }
35
+
36
+ document.addEventListener("mousedown", onPointerDown);
37
+ return () => document.removeEventListener("mousedown", onPointerDown);
38
+ }, [menuOpen]);
39
+
40
+ return (
41
+ <header className="topbar">
42
+ <div className="topbar-brand">
43
+ {logoSrc ? (
44
+ <img
45
+ className="topbar-logo"
46
+ src={logoSrc}
47
+ alt={logoAlt}
48
+ onError={(event) => {
49
+ event.currentTarget.hidden = true;
50
+ }}
51
+ />
52
+ ) : null}
53
+ <h1>{title}</h1>
54
+ </div>
55
+ <div className="topbar-actions" ref={menuRef}>
56
+ {children}
57
+ {onLogout ? (
58
+ <>
59
+ <button
60
+ type="button"
61
+ className="avatar-button"
62
+ aria-haspopup="menu"
63
+ aria-expanded={menuOpen}
64
+ onClick={() => setMenuOpen((open) => !open)}
65
+ title={username || "Account"}
66
+ >
67
+ <span className="avatar">{initial}</span>
68
+ </button>
69
+ {menuOpen ? (
70
+ <div className="avatar-menu" role="menu">
71
+ {username ? <p className="avatar-name">{username}</p> : null}
72
+ <Button
73
+ variant="ghost"
74
+ role="menuitem"
75
+ onClick={() => {
76
+ setMenuOpen(false);
77
+ onLogout();
78
+ }}
79
+ >
80
+ Logout
81
+ </Button>
82
+ </div>
83
+ ) : null}
84
+ </>
85
+ ) : null}
86
+ </div>
87
+ </header>
88
+ );
89
+ }
@@ -0,0 +1,9 @@
1
+ export { Button, type ButtonVariant } from "./Button";
2
+ export { Select, AppSelect, type SelectOption, type AppSelectOption } from "./Select";
3
+ export { DateField } from "./DateField";
4
+ export { Dialog, FormDialog } from "./Dialog";
5
+ export { Snackbar, StatusMessage } from "./Snackbar";
6
+ export { Topbar } from "./Topbar";
7
+ export { SideNav, type SideNavItem } from "./SideNav";
8
+ export { Pagination, type PageSizeOption } from "./Pagination";
9
+ export { FormActions } from "./FormActions";
@@ -0,0 +1,70 @@
1
+ :root {
2
+ /* Canonical aksui tokens */
3
+ --aks-brand: #1e2735;
4
+ --aks-accent: #e67e22;
5
+ --aks-ink: #1e2735;
6
+ --aks-muted: #667085;
7
+ --aks-line: #e6e4df;
8
+ --aks-field: #f6f5f3;
9
+ --aks-card: #ffffff;
10
+ --aks-danger: #b42318;
11
+ --aks-danger-bg: #fff1f0;
12
+ --aks-ok: #067647;
13
+ --aks-ok-bg: #ecfdf3;
14
+ --aks-radius: 8px;
15
+ --aks-row-height: 34px;
16
+ --aks-table-header-height: 36px;
17
+ --aks-side-rail-collapsed: 52px;
18
+ --aks-side-rail-expanded: 280px;
19
+ --aks-preview-panel-width: 300px;
20
+
21
+ /*
22
+ * Compatibility aliases for apps migrating from aksconfigurator_ui tokens.
23
+ * Prefer --aks-* in new code.
24
+ */
25
+ --brand: var(--aks-brand);
26
+ --accent: var(--aks-accent);
27
+ --ink: var(--aks-ink);
28
+ --muted: var(--aks-muted);
29
+ --line: var(--aks-line);
30
+ --field: var(--aks-field);
31
+ --card: var(--aks-card);
32
+ --danger: var(--aks-danger);
33
+ --danger-bg: var(--aks-danger-bg);
34
+ --ok: var(--aks-ok);
35
+ --ok-bg: var(--aks-ok-bg);
36
+ --radius: var(--aks-radius);
37
+ --row-height: var(--aks-row-height);
38
+ --table-header-height: var(--aks-table-header-height);
39
+ --side-rail-collapsed: var(--aks-side-rail-collapsed);
40
+ --side-rail-expanded: var(--aks-side-rail-expanded);
41
+ --preview-panel-width: var(--aks-preview-panel-width);
42
+
43
+ color-scheme: light;
44
+ }
45
+
46
+ *,
47
+ *::before,
48
+ *::after {
49
+ box-sizing: border-box;
50
+ }
51
+
52
+ html,
53
+ body {
54
+ margin: 0;
55
+ width: 100%;
56
+ min-height: 100%;
57
+ font-family: "Segoe UI Variable", "Segoe UI", system-ui, sans-serif;
58
+ }
59
+
60
+ body {
61
+ min-height: 100dvh;
62
+ color: var(--aks-ink);
63
+ background: #f3f2ef;
64
+ }
65
+
66
+ .aksui-root {
67
+ font-family: "Segoe UI Variable", "Segoe UI", system-ui, sans-serif;
68
+ color: var(--aks-ink);
69
+ background: #f3f2ef;
70
+ }
@@ -0,0 +1,3 @@
1
+ /** Theme tokens live in aksui.css. Import `@mshuja288/aksuicore/styles.css` in the host app. */
2
+ export const aksuiThemeVersion = "0.2.0";
3
+ export { aksuiTokens, type AksuiTokenName } from "./tokens";