@hyddenlabs/hydn-ui 0.3.14 → 0.3.16
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/components/forms/button/button-with-icon.d.ts +3 -0
- package/dist/components/forms/button/button-with-icon.d.ts.map +1 -1
- package/dist/components/forms/button/button-with-icon.js +23 -2
- package/dist/components/forms/button/button-with-icon.js.map +1 -1
- package/dist/components/forms/button/button.d.ts +4 -0
- package/dist/components/forms/button/button.d.ts.map +1 -1
- package/dist/components/forms/button/button.js +20 -6
- package/dist/components/forms/button/button.js.map +1 -1
- package/dist/components/forms/button/icon-button.d.ts +2 -0
- package/dist/components/forms/button/icon-button.d.ts.map +1 -1
- package/dist/components/forms/button/icon-button.js +3 -0
- package/dist/components/forms/button/icon-button.js.map +1 -1
- package/dist/components/forms/button/inline-button.d.ts +2 -0
- package/dist/components/forms/button/inline-button.d.ts.map +1 -1
- package/dist/components/forms/button/inline-button.js +8 -3
- package/dist/components/forms/button/inline-button.js.map +1 -1
- package/dist/components/layout/left-nav-layout/left-nav-item.d.ts +3 -6
- package/dist/components/layout/left-nav-layout/left-nav-item.d.ts.map +1 -1
- package/dist/components/layout/left-nav-layout/left-nav-item.js +17 -38
- package/dist/components/layout/left-nav-layout/left-nav-item.js.map +1 -1
- package/dist/components/layout/left-nav-layout/left-nav-layout.d.ts +21 -27
- package/dist/components/layout/left-nav-layout/left-nav-layout.d.ts.map +1 -1
- package/dist/components/layout/left-nav-layout/left-nav-layout.js +94 -140
- package/dist/components/layout/left-nav-layout/left-nav-layout.js.map +1 -1
- package/dist/components/layout/left-nav-layout/left-nav-section.d.ts.map +1 -1
- package/dist/components/layout/left-nav-layout/left-nav-section.js +10 -10
- package/dist/components/layout/left-nav-layout/left-nav-section.js.map +1 -1
- package/dist/components/navigation/navbar/navbar.js +2 -2
- package/dist/components/navigation/navbar/navbar.js.map +1 -1
- package/dist/style.css +1 -1
- package/dist/theme/tokens.d.ts.map +1 -1
- package/dist/theme/tokens.js +37 -37
- package/dist/theme/tokens.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,10 @@ export interface ButtonWithIconProps extends Omit<BaseButtonProps, 'icon' | 'sty
|
|
|
6
6
|
icon?: string;
|
|
7
7
|
iconSize?: IconSize;
|
|
8
8
|
iconColor?: ColorVariant | 'currentColor';
|
|
9
|
+
/** Visual style for the button - renamed from `style` to avoid HTML attribute conflict */
|
|
9
10
|
buttonStyle?: BaseButtonProps['style'];
|
|
11
|
+
/** Visual style to apply on hover - alias of `hoverStyle`, consistent with `buttonStyle` naming */
|
|
12
|
+
hoverButtonStyle?: BaseButtonProps['hoverStyle'];
|
|
10
13
|
children?: React.ReactNode;
|
|
11
14
|
}
|
|
12
15
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button-with-icon.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/button/button-with-icon.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAQ,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAe,EAAE,WAAW,IAAI,eAAe,EAAE,MAAM,UAAU,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,SAAS,CAAC,EAAE,YAAY,GAAG,cAAc,CAAC;IAC1C,WAAW,CAAC,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;IACvC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED;;;GAGG;AACH,QAAA,MAAM,cAAc,+
|
|
1
|
+
{"version":3,"file":"button-with-icon.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/button/button-with-icon.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAQ,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAe,EAAE,WAAW,IAAI,eAAe,EAAE,MAAM,UAAU,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC;IAClF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,SAAS,CAAC,EAAE,YAAY,GAAG,cAAc,CAAC;IAC1C,0FAA0F;IAC1F,WAAW,CAAC,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;IACvC,mGAAmG;IACnG,gBAAgB,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IACjD,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED;;;GAGG;AACH,QAAA,MAAM,cAAc,+FA6BnB,CAAC;AAIF,eAAe,cAAc,CAAC"}
|
|
@@ -3,9 +3,30 @@ import React from "react";
|
|
|
3
3
|
import { Icon } from "../../system/icon/icon.js";
|
|
4
4
|
import Button from "./button.js";
|
|
5
5
|
const ButtonWithIcon = React.forwardRef(
|
|
6
|
-
({
|
|
6
|
+
({
|
|
7
|
+
icon,
|
|
8
|
+
iconSize = "md",
|
|
9
|
+
iconColor = "currentColor",
|
|
10
|
+
iconPosition = "left",
|
|
11
|
+
buttonStyle,
|
|
12
|
+
hoverButtonStyle,
|
|
13
|
+
hoverStyle,
|
|
14
|
+
children,
|
|
15
|
+
...rest
|
|
16
|
+
}, ref) => {
|
|
7
17
|
const iconNode = icon ? /* @__PURE__ */ jsx(Icon, { name: icon, size: iconSize, color: iconColor }) : null;
|
|
8
|
-
return /* @__PURE__ */ jsx(
|
|
18
|
+
return /* @__PURE__ */ jsx(
|
|
19
|
+
Button,
|
|
20
|
+
{
|
|
21
|
+
ref,
|
|
22
|
+
icon: iconNode,
|
|
23
|
+
iconPosition,
|
|
24
|
+
style: buttonStyle,
|
|
25
|
+
hoverStyle: hoverButtonStyle ?? hoverStyle,
|
|
26
|
+
...rest,
|
|
27
|
+
children
|
|
28
|
+
}
|
|
29
|
+
);
|
|
9
30
|
}
|
|
10
31
|
);
|
|
11
32
|
ButtonWithIcon.displayName = "ButtonWithIcon";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button-with-icon.js","sources":["../../../../src/components/forms/button/button-with-icon.tsx"],"sourcesContent":["import React from 'react';\nimport { Icon, IconSize } from '../../system/icon';\nimport Button, { ButtonProps as BaseButtonProps } from './button';\nimport { ColorVariant } from '../../../theme/tokens';\n\nexport interface ButtonWithIconProps extends Omit<BaseButtonProps, 'icon' | 'style'> {\n icon?: string;\n iconSize?: IconSize;\n iconColor?: ColorVariant | 'currentColor';\n buttonStyle?: BaseButtonProps['style'];\n children?: React.ReactNode;\n}\n\n/**\n * ButtonWithIcon - Button using Icon wrapper for consistent icon sizing\n * Usage: <ButtonWithIcon icon=\"plus\" iconSize=\"md\">Add</ButtonWithIcon>\n */\nconst ButtonWithIcon = React.forwardRef<HTMLButtonElement, ButtonWithIconProps>(\n (\n {
|
|
1
|
+
{"version":3,"file":"button-with-icon.js","sources":["../../../../src/components/forms/button/button-with-icon.tsx"],"sourcesContent":["import React from 'react';\nimport { Icon, IconSize } from '../../system/icon';\nimport Button, { ButtonProps as BaseButtonProps } from './button';\nimport { ColorVariant } from '../../../theme/tokens';\n\nexport interface ButtonWithIconProps extends Omit<BaseButtonProps, 'icon' | 'style'> {\n icon?: string;\n iconSize?: IconSize;\n iconColor?: ColorVariant | 'currentColor';\n /** Visual style for the button - renamed from `style` to avoid HTML attribute conflict */\n buttonStyle?: BaseButtonProps['style'];\n /** Visual style to apply on hover - alias of `hoverStyle`, consistent with `buttonStyle` naming */\n hoverButtonStyle?: BaseButtonProps['hoverStyle'];\n children?: React.ReactNode;\n}\n\n/**\n * ButtonWithIcon - Button using Icon wrapper for consistent icon sizing\n * Usage: <ButtonWithIcon icon=\"plus\" iconSize=\"md\">Add</ButtonWithIcon>\n */\nconst ButtonWithIcon = React.forwardRef<HTMLButtonElement, ButtonWithIconProps>(\n (\n {\n icon,\n iconSize = 'md',\n iconColor = 'currentColor',\n iconPosition = 'left',\n buttonStyle,\n hoverButtonStyle,\n hoverStyle,\n children,\n ...rest\n },\n ref\n ) => {\n const iconNode = icon ? <Icon name={icon} size={iconSize} color={iconColor} /> : null;\n return (\n <Button\n ref={ref}\n icon={iconNode}\n iconPosition={iconPosition}\n style={buttonStyle}\n hoverStyle={hoverButtonStyle ?? hoverStyle}\n {...rest}\n >\n {children}\n </Button>\n );\n }\n);\n\nButtonWithIcon.displayName = 'ButtonWithIcon';\n\nexport default ButtonWithIcon;\n"],"names":[],"mappings":";;;;AAoBA,MAAM,iBAAiB,MAAM;AAAA,EAC3B,CACE;AAAA,IACE;AAAA,IACA,WAAW;AAAA,IACX,YAAY;AAAA,IACZ,eAAe;AAAA,IACf;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,WAAW,OAAO,oBAAC,MAAA,EAAK,MAAM,MAAM,MAAM,UAAU,OAAO,UAAA,CAAW,IAAK;AACjF,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,MAAM;AAAA,QACN;AAAA,QACA,OAAO;AAAA,QACP,YAAY,oBAAoB;AAAA,QAC/B,GAAG;AAAA,QAEH;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AACF;AAEA,eAAe,cAAc;"}
|
|
@@ -46,6 +46,10 @@ export type ButtonProps = {
|
|
|
46
46
|
align?: Alignment;
|
|
47
47
|
/** Applies active/pressed state styling */
|
|
48
48
|
active?: boolean;
|
|
49
|
+
/** Color variant to use when the button is hovered - overrides `variant` on hover */
|
|
50
|
+
hoverVariant?: ColorVariant;
|
|
51
|
+
/** Visual style to use when the button is hovered - overrides `style` on hover */
|
|
52
|
+
hoverStyle?: 'solid' | 'outline' | 'ghost' | 'link' | 'soft' | 'none';
|
|
49
53
|
/** ARIA expanded state - indicates if controlled element is expanded */
|
|
50
54
|
'aria-expanded'?: boolean;
|
|
51
55
|
/** ARIA haspopup - indicates element has popup menu/dialog */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/button/button.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/button/button.tsx"],"names":[],"mappings":"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"}
|
|
@@ -27,6 +27,8 @@ const Button = React.forwardRef(
|
|
|
27
27
|
wide = false,
|
|
28
28
|
align,
|
|
29
29
|
active = false,
|
|
30
|
+
hoverVariant,
|
|
31
|
+
hoverStyle: hoverStyleProp,
|
|
30
32
|
"aria-expanded": ariaExpanded,
|
|
31
33
|
"aria-haspopup": ariaHaspopup,
|
|
32
34
|
"aria-controls": ariaControls,
|
|
@@ -36,12 +38,24 @@ const Button = React.forwardRef(
|
|
|
36
38
|
rel
|
|
37
39
|
}, ref) => {
|
|
38
40
|
const isIconOnly = icon && !children;
|
|
41
|
+
const hasHoverOverride = Boolean(hoverVariant || hoverStyleProp);
|
|
42
|
+
const [isHovered, setIsHovered] = React.useState(false);
|
|
43
|
+
const effectiveVariant = hasHoverOverride && isHovered ? hoverVariant ?? variant : variant;
|
|
44
|
+
const effectiveStyle = hasHoverOverride && isHovered ? hoverStyleProp ?? style : style;
|
|
45
|
+
const handleMouseEnter = hasHoverOverride ? (e) => {
|
|
46
|
+
setIsHovered(true);
|
|
47
|
+
onMouseEnter?.(e);
|
|
48
|
+
} : onMouseEnter;
|
|
49
|
+
const handleMouseLeave = hasHoverOverride ? (e) => {
|
|
50
|
+
setIsHovered(false);
|
|
51
|
+
onMouseLeave?.(e);
|
|
52
|
+
} : onMouseLeave;
|
|
39
53
|
if (isIconOnly && !ariaLabel) {
|
|
40
54
|
console.warn("Button: Icon-only buttons require an ariaLabel for accessibility");
|
|
41
55
|
}
|
|
42
56
|
const getStyleClasses = () => {
|
|
43
|
-
const variantKey =
|
|
44
|
-
switch (
|
|
57
|
+
const variantKey = effectiveVariant;
|
|
58
|
+
switch (effectiveStyle) {
|
|
45
59
|
case "outline":
|
|
46
60
|
return `${colorVariants.outline[variantKey]}`;
|
|
47
61
|
case "ghost":
|
|
@@ -113,9 +127,9 @@ const Button = React.forwardRef(
|
|
|
113
127
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
128
|
onClick,
|
|
115
129
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
-
onMouseEnter,
|
|
130
|
+
onMouseEnter: handleMouseEnter,
|
|
117
131
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
118
|
-
onMouseLeave,
|
|
132
|
+
onMouseLeave: handleMouseLeave,
|
|
119
133
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
120
134
|
onMouseDown,
|
|
121
135
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -133,8 +147,8 @@ const Button = React.forwardRef(
|
|
|
133
147
|
ref,
|
|
134
148
|
type,
|
|
135
149
|
onClick,
|
|
136
|
-
onMouseEnter,
|
|
137
|
-
onMouseLeave,
|
|
150
|
+
onMouseEnter: handleMouseEnter,
|
|
151
|
+
onMouseLeave: handleMouseLeave,
|
|
138
152
|
onMouseDown,
|
|
139
153
|
onBlur,
|
|
140
154
|
"aria-label": ariaLabel,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"button.js","sources":["../../../../src/components/forms/button/button.tsx"],"sourcesContent":["import React from 'react';\nimport { Link } from 'react-router-dom';\n\nimport { interactiveSizes, InteractiveSize, InputWidth, inputWidthSizes } from '../../../theme/size-tokens';\nimport { Alignment, colorVariants, inputAlignClasses, ColorVariant } from '../../../theme/tokens';\n\nexport type ButtonProps = {\n /** Button label content - text, icons, or other elements */\n children?: React.ReactNode;\n /** Click event handler */\n onClick?: (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;\n /** Mouse enter event handler */\n onMouseEnter?: (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;\n /** Mouse leave event handler */\n onMouseLeave?: (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;\n /** Mouse down event handler */\n onMouseDown?: (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;\n /** Blur event handler */\n onBlur?: (e: React.FocusEvent<HTMLButtonElement | HTMLAnchorElement>) => void;\n /** Accessible label for icon-only buttons (required when no children) */\n ariaLabel?: string;\n /** Disables button interaction and applies disabled styling */\n disabled?: boolean;\n /** HTML button type attribute (ignored when `to` or `href` is set) */\n type?: 'button' | 'submit' | 'reset';\n /** Additional CSS classes applied to the button */\n className?: string;\n /** Icon element to display alongside button text */\n icon?: React.ReactNode;\n /** Position of the icon relative to button text */\n iconPosition?: 'left' | 'right';\n /** Color variant affecting button appearance */\n variant?: ColorVariant;\n /** Visual style modifier - solid (filled), outline (bordered), ghost (transparent), link (text-only), soft (subtle background), or none (no background at all) */\n style?: 'solid' | 'outline' | 'ghost' | 'link' | 'soft' | 'none';\n /** Size variant - uses unified size system (xs, sm, md, lg, xl) */\n size?: InteractiveSize;\n /** Border radius style - default (rounded), pill (fully rounded), square (sharp corners), circle (for icon-only buttons) */\n rounded?: 'default' | 'pill' | 'square' | 'circle';\n /** Shows loading spinner and disables interaction */\n loading?: boolean;\n /** Width of the button */\n width?: InputWidth;\n /** When true, button spans full width of container (same as width=\"full\") */\n fullWidth?: boolean;\n /** When true, button uses wider min-width (good for prominent actions) */\n wide?: boolean;\n /** Aligns the button within its container: left, center, or right */\n align?: Alignment;\n /** Applies active/pressed state styling */\n active?: boolean;\n /** ARIA expanded state - indicates if controlled element is expanded */\n 'aria-expanded'?: boolean;\n /** ARIA haspopup - indicates element has popup menu/dialog */\n 'aria-haspopup'?: boolean | 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';\n /** ARIA controls - ID of element controlled by this button */\n 'aria-controls'?: string;\n /**\n * React Router path. When provided the component renders as a `<Link>` (anchor tag)\n * instead of a `<button>`, giving you router-aware navigation with full button styling.\n */\n to?: string;\n /**\n * Plain URL. When provided (and `to` is not set) the component renders as an `<a>` tag.\n * Use `to` instead when navigating within the app so React Router manages the history.\n */\n href?: string;\n /** Link target attribute (`_blank`, `_self`, etc.) – only used when `to` or `href` is set */\n target?: React.HTMLAttributeAnchorTarget;\n /** Rel attribute for the anchor – defaults to `\"noreferrer noopener\"` when `target=\"_blank\"` */\n rel?: string;\n};\n\n/**\n * Accessible Button component\n * - Color variants: neutral, primary, accent, info, success, warning, error\n * - Style modifiers: solid (default), outline, ghost, link, soft\n * - Sizes: xs, sm, md (default), lg, xl\n * - Supports icons with flexible positioning (left/right)\n * - Icon-only buttons require `ariaLabel` for accessibility\n * - Pass `to` to render as a React Router `<Link>`, or `href` for a plain `<a>`, keeping full button styling\n */\nconst Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(\n (\n {\n children,\n onClick,\n onMouseEnter,\n onMouseLeave,\n onMouseDown,\n onBlur,\n ariaLabel,\n disabled = false,\n type = 'button',\n className = '',\n icon,\n iconPosition = 'left',\n variant = 'neutral',\n style = 'solid',\n size = 'md',\n rounded = 'default',\n loading = false,\n width = 'auto',\n fullWidth = false,\n wide = false,\n align,\n active = false,\n 'aria-expanded': ariaExpanded,\n 'aria-haspopup': ariaHaspopup,\n 'aria-controls': ariaControls,\n to,\n href,\n target,\n rel\n },\n ref\n ) => {\n const isIconOnly = icon && !children;\n\n // Icon-only buttons MUST have an aria-label for accessibility\n if (isIconOnly && !ariaLabel) {\n // eslint-disable-next-line no-console\n console.warn('Button: Icon-only buttons require an ariaLabel for accessibility');\n }\n\n // Style modifiers\n const getStyleClasses = () => {\n const variantKey = variant as keyof typeof colorVariants.solid;\n\n switch (style) {\n case 'outline':\n return `${colorVariants.outline[variantKey]}`;\n case 'ghost':\n return colorVariants.ghost[variantKey];\n case 'link':\n return colorVariants.link[variantKey];\n case 'soft':\n return colorVariants.soft[variantKey];\n case 'none':\n return colorVariants.none[variantKey];\n case 'solid':\n default:\n return `${colorVariants.solid[variantKey]} shadow-sm hover:shadow-md`;\n }\n };\n\n // Sizes - from unified size system\n const sizeConfig = interactiveSizes[size];\n const sizeClasses = `${sizeConfig.height} ${sizeConfig.padding} ${sizeConfig.text}`;\n\n const roundedClasses = {\n default: 'rounded-md',\n pill: 'rounded-full',\n square: 'rounded-none aspect-square',\n circle: 'rounded-full aspect-square'\n };\n\n // Show loading spinner or icon\n const displayIcon = loading ? (\n <svg className=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\"></circle>\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n ></path>\n </svg>\n ) : (\n icon\n );\n\n const styleClasses = getStyleClasses();\n const effectiveWidth = fullWidth ? 'full' : wide ? 'xl' : width;\n const widthClasses = inputWidthSizes[effectiveWidth];\n const alignmentClass = align ? inputAlignClasses[align] : '';\n const activeClasses = active ? 'active:scale-95' : '';\n\n const shouldRenderIconSlots = !isIconOnly && Boolean(displayIcon);\n const cloneIcon = () => {\n if (!displayIcon) return null;\n if (React.isValidElement(displayIcon)) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return React.cloneElement(displayIcon as React.ReactElement<any>, { 'aria-hidden': false });\n }\n return displayIcon;\n };\n\n const showLeftIcon = shouldRenderIconSlots && iconPosition === 'left';\n const showRightIcon = shouldRenderIconSlots && iconPosition === 'right';\n\n // Check if className contains display utilities to avoid conflicts\n const hasDisplayOverride =\n className.includes('hidden') ||\n className.includes('inline') ||\n className.includes('block') ||\n className.includes('flex');\n const baseDisplayClass = hasDisplayOverride ? '' : 'inline-flex';\n\n const isDisabled = disabled || loading;\n\n const combinedClassName = `${baseDisplayClass} items-center justify-center ${alignmentClass} ${roundedClasses[rounded]} font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 ${styleClasses} ${sizeClasses} ${\n isIconOnly ? 'p-0' : ''\n } ${widthClasses} ${activeClasses} ${isDisabled && (to || href) ? 'pointer-events-none opacity-50' : ''} ${className}`;\n\n const innerContent = (\n <>\n {showLeftIcon && <span className=\"flex shrink-0 items-center mr-2\">{cloneIcon()}</span>}\n {isIconOnly ? (\n <span className=\"inline-flex items-center justify-center\">{displayIcon}</span>\n ) : (\n <span className=\"inline-flex flex-1 justify-center text-center\">{children}</span>\n )}\n {showRightIcon && <span className=\"flex shrink-0 items-center ml-2\">{cloneIcon()}</span>}\n </>\n );\n\n const sharedLinkProps = {\n 'aria-label': ariaLabel,\n 'aria-expanded': ariaExpanded,\n 'aria-haspopup': ariaHaspopup,\n 'aria-controls': ariaControls,\n 'aria-disabled': isDisabled || undefined,\n tabIndex: isDisabled ? -1 : undefined,\n target,\n rel: rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined),\n className: combinedClassName,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onClick: onClick as any,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onMouseEnter: onMouseEnter as any,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onMouseLeave: onMouseLeave as any,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onMouseDown: onMouseDown as any,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onBlur: onBlur as any\n };\n\n if (to) {\n return (\n <Link to={to} ref={ref as React.Ref<HTMLAnchorElement>} {...sharedLinkProps}>\n {innerContent}\n </Link>\n );\n }\n\n if (href) {\n return (\n <a href={href} ref={ref as React.Ref<HTMLAnchorElement>} {...sharedLinkProps}>\n {innerContent}\n </a>\n );\n }\n\n return (\n <button\n ref={ref as React.Ref<HTMLButtonElement>}\n type={type}\n onClick={onClick}\n onMouseEnter={onMouseEnter}\n onMouseLeave={onMouseLeave}\n onMouseDown={onMouseDown}\n onBlur={onBlur}\n aria-label={ariaLabel}\n aria-expanded={ariaExpanded}\n aria-haspopup={ariaHaspopup}\n aria-controls={ariaControls}\n disabled={isDisabled}\n className={combinedClassName}\n >\n {innerContent}\n </button>\n );\n }\n);\n\nButton.displayName = 'Button';\n\nexport default Button;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"button.js","sources":["../../../../src/components/forms/button/button.tsx"],"sourcesContent":["import React from 'react';\nimport { Link } from 'react-router-dom';\n\nimport { interactiveSizes, InteractiveSize, InputWidth, inputWidthSizes } from '../../../theme/size-tokens';\nimport { Alignment, colorVariants, inputAlignClasses, ColorVariant } from '../../../theme/tokens';\n\nexport type ButtonProps = {\n /** Button label content - text, icons, or other elements */\n children?: React.ReactNode;\n /** Click event handler */\n onClick?: (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;\n /** Mouse enter event handler */\n onMouseEnter?: (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;\n /** Mouse leave event handler */\n onMouseLeave?: (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;\n /** Mouse down event handler */\n onMouseDown?: (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;\n /** Blur event handler */\n onBlur?: (e: React.FocusEvent<HTMLButtonElement | HTMLAnchorElement>) => void;\n /** Accessible label for icon-only buttons (required when no children) */\n ariaLabel?: string;\n /** Disables button interaction and applies disabled styling */\n disabled?: boolean;\n /** HTML button type attribute (ignored when `to` or `href` is set) */\n type?: 'button' | 'submit' | 'reset';\n /** Additional CSS classes applied to the button */\n className?: string;\n /** Icon element to display alongside button text */\n icon?: React.ReactNode;\n /** Position of the icon relative to button text */\n iconPosition?: 'left' | 'right';\n /** Color variant affecting button appearance */\n variant?: ColorVariant;\n /** Visual style modifier - solid (filled), outline (bordered), ghost (transparent), link (text-only), soft (subtle background), or none (no background at all) */\n style?: 'solid' | 'outline' | 'ghost' | 'link' | 'soft' | 'none';\n /** Size variant - uses unified size system (xs, sm, md, lg, xl) */\n size?: InteractiveSize;\n /** Border radius style - default (rounded), pill (fully rounded), square (sharp corners), circle (for icon-only buttons) */\n rounded?: 'default' | 'pill' | 'square' | 'circle';\n /** Shows loading spinner and disables interaction */\n loading?: boolean;\n /** Width of the button */\n width?: InputWidth;\n /** When true, button spans full width of container (same as width=\"full\") */\n fullWidth?: boolean;\n /** When true, button uses wider min-width (good for prominent actions) */\n wide?: boolean;\n /** Aligns the button within its container: left, center, or right */\n align?: Alignment;\n /** Applies active/pressed state styling */\n active?: boolean;\n /** Color variant to use when the button is hovered - overrides `variant` on hover */\n hoverVariant?: ColorVariant;\n /** Visual style to use when the button is hovered - overrides `style` on hover */\n hoverStyle?: 'solid' | 'outline' | 'ghost' | 'link' | 'soft' | 'none';\n /** ARIA expanded state - indicates if controlled element is expanded */\n 'aria-expanded'?: boolean;\n /** ARIA haspopup - indicates element has popup menu/dialog */\n 'aria-haspopup'?: boolean | 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';\n /** ARIA controls - ID of element controlled by this button */\n 'aria-controls'?: string;\n /**\n * React Router path. When provided the component renders as a `<Link>` (anchor tag)\n * instead of a `<button>`, giving you router-aware navigation with full button styling.\n */\n to?: string;\n /**\n * Plain URL. When provided (and `to` is not set) the component renders as an `<a>` tag.\n * Use `to` instead when navigating within the app so React Router manages the history.\n */\n href?: string;\n /** Link target attribute (`_blank`, `_self`, etc.) – only used when `to` or `href` is set */\n target?: React.HTMLAttributeAnchorTarget;\n /** Rel attribute for the anchor – defaults to `\"noreferrer noopener\"` when `target=\"_blank\"` */\n rel?: string;\n};\n\n/**\n * Accessible Button component\n * - Color variants: neutral, primary, accent, info, success, warning, error\n * - Style modifiers: solid (default), outline, ghost, link, soft\n * - Sizes: xs, sm, md (default), lg, xl\n * - Supports icons with flexible positioning (left/right)\n * - Icon-only buttons require `ariaLabel` for accessibility\n * - Pass `to` to render as a React Router `<Link>`, or `href` for a plain `<a>`, keeping full button styling\n */\nconst Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>(\n (\n {\n children,\n onClick,\n onMouseEnter,\n onMouseLeave,\n onMouseDown,\n onBlur,\n ariaLabel,\n disabled = false,\n type = 'button',\n className = '',\n icon,\n iconPosition = 'left',\n variant = 'neutral',\n style = 'solid',\n size = 'md',\n rounded = 'default',\n loading = false,\n width = 'auto',\n fullWidth = false,\n wide = false,\n align,\n active = false,\n hoverVariant,\n hoverStyle: hoverStyleProp,\n 'aria-expanded': ariaExpanded,\n 'aria-haspopup': ariaHaspopup,\n 'aria-controls': ariaControls,\n to,\n href,\n target,\n rel\n },\n ref\n ) => {\n const isIconOnly = icon && !children;\n\n // Hover override tracking\n const hasHoverOverride = Boolean(hoverVariant || hoverStyleProp);\n const [isHovered, setIsHovered] = React.useState(false);\n const effectiveVariant = hasHoverOverride && isHovered ? (hoverVariant ?? variant) : variant;\n const effectiveStyle = hasHoverOverride && isHovered ? (hoverStyleProp ?? style) : style;\n\n const handleMouseEnter = hasHoverOverride\n ? (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {\n setIsHovered(true);\n onMouseEnter?.(e);\n }\n : onMouseEnter;\n\n const handleMouseLeave = hasHoverOverride\n ? (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {\n setIsHovered(false);\n onMouseLeave?.(e);\n }\n : onMouseLeave;\n\n // Icon-only buttons MUST have an aria-label for accessibility\n if (isIconOnly && !ariaLabel) {\n // eslint-disable-next-line no-console\n console.warn('Button: Icon-only buttons require an ariaLabel for accessibility');\n }\n\n // Style modifiers\n const getStyleClasses = () => {\n const variantKey = effectiveVariant as keyof typeof colorVariants.solid;\n\n switch (effectiveStyle) {\n case 'outline':\n return `${colorVariants.outline[variantKey]}`;\n case 'ghost':\n return colorVariants.ghost[variantKey];\n case 'link':\n return colorVariants.link[variantKey];\n case 'soft':\n return colorVariants.soft[variantKey];\n case 'none':\n return colorVariants.none[variantKey];\n case 'solid':\n default:\n return `${colorVariants.solid[variantKey]} shadow-sm hover:shadow-md`;\n }\n };\n\n // Sizes - from unified size system\n const sizeConfig = interactiveSizes[size];\n const sizeClasses = `${sizeConfig.height} ${sizeConfig.padding} ${sizeConfig.text}`;\n\n const roundedClasses = {\n default: 'rounded-md',\n pill: 'rounded-full',\n square: 'rounded-none aspect-square',\n circle: 'rounded-full aspect-square'\n };\n\n // Show loading spinner or icon\n const displayIcon = loading ? (\n <svg className=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\">\n <circle className=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"4\"></circle>\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n ></path>\n </svg>\n ) : (\n icon\n );\n\n const styleClasses = getStyleClasses();\n const effectiveWidth = fullWidth ? 'full' : wide ? 'xl' : width;\n const widthClasses = inputWidthSizes[effectiveWidth];\n const alignmentClass = align ? inputAlignClasses[align] : '';\n const activeClasses = active ? 'active:scale-95' : '';\n\n const shouldRenderIconSlots = !isIconOnly && Boolean(displayIcon);\n const cloneIcon = () => {\n if (!displayIcon) return null;\n if (React.isValidElement(displayIcon)) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return React.cloneElement(displayIcon as React.ReactElement<any>, { 'aria-hidden': false });\n }\n return displayIcon;\n };\n\n const showLeftIcon = shouldRenderIconSlots && iconPosition === 'left';\n const showRightIcon = shouldRenderIconSlots && iconPosition === 'right';\n\n // Check if className contains display utilities to avoid conflicts\n const hasDisplayOverride =\n className.includes('hidden') ||\n className.includes('inline') ||\n className.includes('block') ||\n className.includes('flex');\n const baseDisplayClass = hasDisplayOverride ? '' : 'inline-flex';\n\n const isDisabled = disabled || loading;\n\n const combinedClassName = `${baseDisplayClass} items-center justify-center ${alignmentClass} ${roundedClasses[rounded]} font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 ${styleClasses} ${sizeClasses} ${\n isIconOnly ? 'p-0' : ''\n } ${widthClasses} ${activeClasses} ${isDisabled && (to || href) ? 'pointer-events-none opacity-50' : ''} ${className}`;\n\n const innerContent = (\n <>\n {showLeftIcon && <span className=\"flex shrink-0 items-center mr-2\">{cloneIcon()}</span>}\n {isIconOnly ? (\n <span className=\"inline-flex items-center justify-center\">{displayIcon}</span>\n ) : (\n <span className=\"inline-flex flex-1 justify-center text-center\">{children}</span>\n )}\n {showRightIcon && <span className=\"flex shrink-0 items-center ml-2\">{cloneIcon()}</span>}\n </>\n );\n\n const sharedLinkProps = {\n 'aria-label': ariaLabel,\n 'aria-expanded': ariaExpanded,\n 'aria-haspopup': ariaHaspopup,\n 'aria-controls': ariaControls,\n 'aria-disabled': isDisabled || undefined,\n tabIndex: isDisabled ? -1 : undefined,\n target,\n rel: rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined),\n className: combinedClassName,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onClick: onClick as any,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onMouseEnter: handleMouseEnter as any,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onMouseLeave: handleMouseLeave as any,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onMouseDown: onMouseDown as any,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n onBlur: onBlur as any\n };\n\n if (to) {\n return (\n <Link to={to} ref={ref as React.Ref<HTMLAnchorElement>} {...sharedLinkProps}>\n {innerContent}\n </Link>\n );\n }\n\n if (href) {\n return (\n <a href={href} ref={ref as React.Ref<HTMLAnchorElement>} {...sharedLinkProps}>\n {innerContent}\n </a>\n );\n }\n\n return (\n <button\n ref={ref as React.Ref<HTMLButtonElement>}\n type={type}\n onClick={onClick}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n onMouseDown={onMouseDown}\n onBlur={onBlur}\n aria-label={ariaLabel}\n aria-expanded={ariaExpanded}\n aria-haspopup={ariaHaspopup}\n aria-controls={ariaControls}\n disabled={isDisabled}\n className={combinedClassName}\n >\n {innerContent}\n </button>\n );\n }\n);\n\nButton.displayName = 'Button';\n\nexport default Button;\n"],"names":[],"mappings":"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"}
|
|
@@ -8,6 +8,8 @@ export interface IconButtonProps extends Omit<BaseButtonProps, 'icon' | 'style'
|
|
|
8
8
|
iconSize?: IconSize;
|
|
9
9
|
iconColor?: ColorVariant | 'currentColor';
|
|
10
10
|
buttonStyle?: BaseButtonProps['style'];
|
|
11
|
+
/** Visual style to apply on hover - alias of `hoverStyle`, consistent with `buttonStyle` naming */
|
|
12
|
+
hoverButtonStyle?: BaseButtonProps['hoverStyle'];
|
|
11
13
|
/** Remove the default button sizing so the icon renders without extra padding */
|
|
12
14
|
noPadding?: boolean;
|
|
13
15
|
/** Icon to display on hover (if not provided, hover will keep the same icon) */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"icon-button.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/button/icon-button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AACxE,OAAO,EAAQ,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAe,EAAE,WAAW,IAAI,eAAe,EAAE,MAAM,UAAU,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;IAC3F,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,SAAS,CAAC,EAAE,YAAY,GAAG,cAAc,CAAC;IAC1C,WAAW,CAAC,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;IACvC,iFAAiF;IACjF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB;AAED;;;GAGG;AACH,QAAA,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"icon-button.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/button/icon-button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AACxE,OAAO,EAAQ,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAe,EAAE,WAAW,IAAI,eAAe,EAAE,MAAM,UAAU,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;IAC3F,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,SAAS,CAAC,EAAE,YAAY,GAAG,cAAc,CAAC;IAC1C,WAAW,CAAC,EAAE,eAAe,CAAC,OAAO,CAAC,CAAC;IACvC,mGAAmG;IACnG,gBAAgB,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IACjD,iFAAiF;IACjF,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB;AAED;;;GAGG;AACH,QAAA,MAAM,UAAU,2FAmGf,CAAC;AAIF,eAAe,UAAU,CAAC"}
|
|
@@ -8,6 +8,8 @@ const IconButton = React.forwardRef(
|
|
|
8
8
|
iconSize = "md",
|
|
9
9
|
iconColor = "currentColor",
|
|
10
10
|
buttonStyle,
|
|
11
|
+
hoverButtonStyle,
|
|
12
|
+
hoverStyle,
|
|
11
13
|
noPadding = false,
|
|
12
14
|
ariaLabel,
|
|
13
15
|
hoverIcon,
|
|
@@ -68,6 +70,7 @@ const IconButton = React.forwardRef(
|
|
|
68
70
|
ref: mergedRef,
|
|
69
71
|
icon: iconNode,
|
|
70
72
|
style: buttonStyle,
|
|
73
|
+
hoverStyle: hoverButtonStyle ?? hoverStyle,
|
|
71
74
|
ariaLabel,
|
|
72
75
|
onMouseEnter: handleMouseEnter,
|
|
73
76
|
onMouseLeave: handleMouseLeave,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"icon-button.js","sources":["../../../../src/components/forms/button/icon-button.tsx"],"sourcesContent":["import React, { useState, useRef, useEffect, useCallback } from 'react';\nimport { Icon, IconSize } from '../../system/icon';\nimport Button, { ButtonProps as BaseButtonProps } from './button';\nimport { ColorVariant } from '../../../theme/tokens';\n\nexport interface IconButtonProps extends Omit<BaseButtonProps, 'icon' | 'style' | 'children'> {\n icon: string;\n ariaLabel: string;\n iconSize?: IconSize;\n iconColor?: ColorVariant | 'currentColor';\n buttonStyle?: BaseButtonProps['style'];\n /** Remove the default button sizing so the icon renders without extra padding */\n noPadding?: boolean;\n /** Icon to display on hover (if not provided, hover will keep the same icon) */\n hoverIcon?: string;\n children?: never;\n}\n\n/**\n * IconButton - Icon-only button using Icon wrapper for consistent icon sizing\n * Usage: <IconButton icon=\"plus\" iconSize=\"md\" ariaLabel=\"Add item\" />\n */\nconst IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(\n (\n {\n icon,\n iconSize = 'md',\n iconColor = 'currentColor',\n buttonStyle,\n noPadding = false,\n ariaLabel,\n hoverIcon,\n onMouseEnter,\n onMouseLeave,\n onClick,\n className = '',\n ...rest\n },\n ref\n ) => {\n const [isHovered, setIsHovered] = useState(false);\n const buttonRef = useRef<HTMLButtonElement | null>(null);\n\n // Merged callback ref that handles both internal and forwarded refs\n const mergedRef = useCallback(\n (node: HTMLButtonElement | null) => {\n // Update internal ref\n buttonRef.current = node;\n\n // Forward to external ref\n if (typeof ref === 'function') {\n ref(node);\n } else if (ref) {\n ref.current = node;\n }\n },\n [ref]\n );\n\n const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {\n setIsHovered(true);\n onMouseEnter?.(e);\n };\n\n const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {\n setIsHovered(false);\n onMouseLeave?.(e);\n };\n\n const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {\n // Reset hover state on click - handles click-drag scenarios\n // where mouseLeave might not fire\n setIsHovered(false);\n onClick?.(e);\n };\n\n // Global mousemove listener to detect when mouse leaves button during drag\n useEffect(() => {\n if (!hoverIcon || !isHovered) return;\n\n const handleGlobalMouseMove = (e: MouseEvent) => {\n if (!buttonRef.current) return;\n\n const rect = buttonRef.current.getBoundingClientRect();\n const isOutside =\n e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom;\n\n if (isOutside) {\n setIsHovered(false);\n }\n };\n\n document.addEventListener('mousemove', handleGlobalMouseMove);\n return () => {\n document.removeEventListener('mousemove', handleGlobalMouseMove);\n };\n }, [hoverIcon, isHovered]);\n\n const displayIcon = isHovered && hoverIcon ? hoverIcon : icon;\n const iconNode = displayIcon ? <Icon name={displayIcon} size={iconSize} color={iconColor} /> : null;\n const paddingClasses = noPadding ? '!px-0 !py-0 !h-auto !min-h-0 !w-auto !min-w-0' : '';\n const hoverClasses = noPadding ? 'hover:!bg-transparent active:!bg-transparent' : '';\n const mergedClassName = [paddingClasses, hoverClasses, className].filter(Boolean).join(' ');\n\n return (\n <Button\n ref={mergedRef}\n icon={iconNode}\n style={buttonStyle}\n ariaLabel={ariaLabel}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n onClick={handleClick}\n className={mergedClassName}\n {...rest}\n />\n );\n }\n);\n\nIconButton.displayName = 'IconButton';\n\nexport default IconButton;\n"],"names":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"icon-button.js","sources":["../../../../src/components/forms/button/icon-button.tsx"],"sourcesContent":["import React, { useState, useRef, useEffect, useCallback } from 'react';\nimport { Icon, IconSize } from '../../system/icon';\nimport Button, { ButtonProps as BaseButtonProps } from './button';\nimport { ColorVariant } from '../../../theme/tokens';\n\nexport interface IconButtonProps extends Omit<BaseButtonProps, 'icon' | 'style' | 'children'> {\n icon: string;\n ariaLabel: string;\n iconSize?: IconSize;\n iconColor?: ColorVariant | 'currentColor';\n buttonStyle?: BaseButtonProps['style'];\n /** Visual style to apply on hover - alias of `hoverStyle`, consistent with `buttonStyle` naming */\n hoverButtonStyle?: BaseButtonProps['hoverStyle'];\n /** Remove the default button sizing so the icon renders without extra padding */\n noPadding?: boolean;\n /** Icon to display on hover (if not provided, hover will keep the same icon) */\n hoverIcon?: string;\n children?: never;\n}\n\n/**\n * IconButton - Icon-only button using Icon wrapper for consistent icon sizing\n * Usage: <IconButton icon=\"plus\" iconSize=\"md\" ariaLabel=\"Add item\" />\n */\nconst IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(\n (\n {\n icon,\n iconSize = 'md',\n iconColor = 'currentColor',\n buttonStyle,\n hoverButtonStyle,\n hoverStyle,\n noPadding = false,\n ariaLabel,\n hoverIcon,\n onMouseEnter,\n onMouseLeave,\n onClick,\n className = '',\n ...rest\n },\n ref\n ) => {\n const [isHovered, setIsHovered] = useState(false);\n const buttonRef = useRef<HTMLButtonElement | null>(null);\n\n // Merged callback ref that handles both internal and forwarded refs\n const mergedRef = useCallback(\n (node: HTMLButtonElement | null) => {\n // Update internal ref\n buttonRef.current = node;\n\n // Forward to external ref\n if (typeof ref === 'function') {\n ref(node);\n } else if (ref) {\n ref.current = node;\n }\n },\n [ref]\n );\n\n const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {\n setIsHovered(true);\n onMouseEnter?.(e);\n };\n\n const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement>) => {\n setIsHovered(false);\n onMouseLeave?.(e);\n };\n\n const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {\n // Reset hover state on click - handles click-drag scenarios\n // where mouseLeave might not fire\n setIsHovered(false);\n onClick?.(e);\n };\n\n // Global mousemove listener to detect when mouse leaves button during drag\n useEffect(() => {\n if (!hoverIcon || !isHovered) return;\n\n const handleGlobalMouseMove = (e: MouseEvent) => {\n if (!buttonRef.current) return;\n\n const rect = buttonRef.current.getBoundingClientRect();\n const isOutside =\n e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom;\n\n if (isOutside) {\n setIsHovered(false);\n }\n };\n\n document.addEventListener('mousemove', handleGlobalMouseMove);\n return () => {\n document.removeEventListener('mousemove', handleGlobalMouseMove);\n };\n }, [hoverIcon, isHovered]);\n\n const displayIcon = isHovered && hoverIcon ? hoverIcon : icon;\n const iconNode = displayIcon ? <Icon name={displayIcon} size={iconSize} color={iconColor} /> : null;\n const paddingClasses = noPadding ? '!px-0 !py-0 !h-auto !min-h-0 !w-auto !min-w-0' : '';\n const hoverClasses = noPadding ? 'hover:!bg-transparent active:!bg-transparent' : '';\n const mergedClassName = [paddingClasses, hoverClasses, className].filter(Boolean).join(' ');\n\n return (\n <Button\n ref={mergedRef}\n icon={iconNode}\n style={buttonStyle}\n hoverStyle={hoverButtonStyle ?? hoverStyle}\n ariaLabel={ariaLabel}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n onClick={handleClick}\n className={mergedClassName}\n {...rest}\n />\n );\n }\n);\n\nIconButton.displayName = 'IconButton';\n\nexport default IconButton;\n"],"names":[],"mappings":";;;;AAwBA,MAAM,aAAa,MAAM;AAAA,EACvB,CACE;AAAA,IACE;AAAA,IACA,WAAW;AAAA,IACX,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,UAAM,YAAY,OAAiC,IAAI;AAGvD,UAAM,YAAY;AAAA,MAChB,CAAC,SAAmC;AAElC,kBAAU,UAAU;AAGpB,YAAI,OAAO,QAAQ,YAAY;AAC7B,cAAI,IAAI;AAAA,QACV,WAAW,KAAK;AACd,cAAI,UAAU;AAAA,QAChB;AAAA,MACF;AAAA,MACA,CAAC,GAAG;AAAA,IAAA;AAGN,UAAM,mBAAmB,CAAC,MAA2C;AACnE,mBAAa,IAAI;AACjB,qBAAe,CAAC;AAAA,IAClB;AAEA,UAAM,mBAAmB,CAAC,MAA2C;AACnE,mBAAa,KAAK;AAClB,qBAAe,CAAC;AAAA,IAClB;AAEA,UAAM,cAAc,CAAC,MAA2C;AAG9D,mBAAa,KAAK;AAClB,gBAAU,CAAC;AAAA,IACb;AAGA,cAAU,MAAM;AACd,UAAI,CAAC,aAAa,CAAC,UAAW;AAE9B,YAAM,wBAAwB,CAAC,MAAkB;AAC/C,YAAI,CAAC,UAAU,QAAS;AAExB,cAAM,OAAO,UAAU,QAAQ,sBAAA;AAC/B,cAAM,YACJ,EAAE,UAAU,KAAK,QAAQ,EAAE,UAAU,KAAK,SAAS,EAAE,UAAU,KAAK,OAAO,EAAE,UAAU,KAAK;AAE9F,YAAI,WAAW;AACb,uBAAa,KAAK;AAAA,QACpB;AAAA,MACF;AAEA,eAAS,iBAAiB,aAAa,qBAAqB;AAC5D,aAAO,MAAM;AACX,iBAAS,oBAAoB,aAAa,qBAAqB;AAAA,MACjE;AAAA,IACF,GAAG,CAAC,WAAW,SAAS,CAAC;AAEzB,UAAM,cAAc,aAAa,YAAY,YAAY;AACzD,UAAM,WAAW,cAAc,oBAAC,MAAA,EAAK,MAAM,aAAa,MAAM,UAAU,OAAO,UAAA,CAAW,IAAK;AAC/F,UAAM,iBAAiB,YAAY,kDAAkD;AACrF,UAAM,eAAe,YAAY,iDAAiD;AAClF,UAAM,kBAAkB,CAAC,gBAAgB,cAAc,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAE1F,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAM;AAAA,QACN,OAAO;AAAA,QACP,YAAY,oBAAoB;AAAA,QAChC;AAAA,QACA,cAAc;AAAA,QACd,cAAc;AAAA,QACd,SAAS;AAAA,QACT,WAAW;AAAA,QACV,GAAG;AAAA,MAAA;AAAA,IAAA;AAAA,EAGV;AACF;AAEA,WAAW,cAAc;"}
|
|
@@ -23,6 +23,8 @@ export type InlineButtonProps = {
|
|
|
23
23
|
underline?: boolean;
|
|
24
24
|
/** Icon size token - defaults to 'xs' to match text size */
|
|
25
25
|
iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
26
|
+
/** Color variant to use when the button is hovered - overrides `variant` on hover */
|
|
27
|
+
hoverVariant?: ColorVariant;
|
|
26
28
|
};
|
|
27
29
|
/**
|
|
28
30
|
* Inline Button - a minimal button designed to be embedded within text
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inline-button.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/button/inline-button.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"inline-button.d.ts","sourceRoot":"","sources":["../../../../src/components/forms/button/inline-button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAGrD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,4CAA4C;IAC5C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,0BAA0B;IAC1B,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;IAC3D,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iCAAiC;IACjC,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;IACrC,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qFAAqF;IACrF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,qDAAqD;IACrD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yDAAyD;IACzD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC5C,qFAAqF;IACrF,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,QAAA,MAAM,YAAY,6FAiEjB,CAAC;AAIF,eAAe,YAAY,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
-
import React from "react";
|
|
2
|
+
import React, { useState } from "react";
|
|
3
3
|
import { Icon } from "../../system/icon/icon.js";
|
|
4
4
|
const InlineButton = React.forwardRef(
|
|
5
5
|
({
|
|
@@ -13,8 +13,11 @@ const InlineButton = React.forwardRef(
|
|
|
13
13
|
variant = "primary",
|
|
14
14
|
loading = false,
|
|
15
15
|
underline = true,
|
|
16
|
-
iconSize = "xs"
|
|
16
|
+
iconSize = "xs",
|
|
17
|
+
hoverVariant
|
|
17
18
|
}, ref) => {
|
|
19
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
20
|
+
const effectiveVariant = hoverVariant && isHovered ? hoverVariant : variant;
|
|
18
21
|
const variantClasses = {
|
|
19
22
|
neutral: "text-neutral hover:text-neutral-hover active:text-neutral-active",
|
|
20
23
|
primary: "text-primary hover:text-primary-hover active:text-primary-active",
|
|
@@ -31,7 +34,7 @@ const InlineButton = React.forwardRef(
|
|
|
31
34
|
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
|
|
32
35
|
disabled || loading ? "cursor-not-allowed opacity-50" : "cursor-pointer",
|
|
33
36
|
underline ? "hover:underline" : "",
|
|
34
|
-
variantClasses[
|
|
37
|
+
variantClasses[effectiveVariant],
|
|
35
38
|
className
|
|
36
39
|
].filter(Boolean).join(" ");
|
|
37
40
|
const iconElement = icon ? /* @__PURE__ */ jsx(Icon, { name: icon, size: iconSize }) : null;
|
|
@@ -41,6 +44,8 @@ const InlineButton = React.forwardRef(
|
|
|
41
44
|
ref,
|
|
42
45
|
type,
|
|
43
46
|
onClick,
|
|
47
|
+
onMouseEnter: hoverVariant ? () => setIsHovered(true) : void 0,
|
|
48
|
+
onMouseLeave: hoverVariant ? () => setIsHovered(false) : void 0,
|
|
44
49
|
disabled: disabled || loading,
|
|
45
50
|
"aria-label": ariaLabel,
|
|
46
51
|
className: baseClasses,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inline-button.js","sources":["../../../../src/components/forms/button/inline-button.tsx"],"sourcesContent":["import React from 'react';\nimport { ColorVariant } from '../../../theme/tokens';\nimport { Icon } from '../../system/icon/icon';\n\nexport type InlineButtonProps = {\n /** Button label content - typically text */\n children: React.ReactNode;\n /** Click event handler */\n onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;\n /** Accessible label (optional, children used by default) */\n ariaLabel?: string;\n /** Disables button interaction and applies disabled styling */\n disabled?: boolean;\n /** HTML button type attribute */\n type?: 'button' | 'submit' | 'reset';\n /** Additional CSS classes applied to the button */\n className?: string;\n /** Icon name string passed to the Icon component to display alongside button text */\n icon?: string;\n /** Color variant - defaults to primary for link-like appearance */\n variant?: ColorVariant;\n /** Shows loading spinner and disables interaction */\n loading?: boolean;\n /** Whether to show underline on hover (default: true) */\n underline?: boolean;\n /** Icon size token - defaults to 'xs' to match text size */\n iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n};\n\n/**\n * Inline Button - a minimal button designed to be embedded within text\n *\n * Perfect for use cases like:\n * - \"By signing in, you agree to our [Terms of Service]\"\n * - \"Learn more about [pricing]\"\n * - Inline actions within paragraphs\n *\n * Features:\n * - No background or padding by default\n * - Inherits surrounding text sizing\n * - Optional icon support\n * - Color variants for different contexts\n * - Hover underline (can be disabled)\n * - Fully accessible\n */\nconst InlineButton = React.forwardRef<HTMLButtonElement, InlineButtonProps>(\n (\n {\n children,\n onClick,\n ariaLabel,\n disabled = false,\n type = 'button',\n className = '',\n icon,\n variant = 'primary',\n loading = false,\n underline = true,\n iconSize = 'xs'\n },\n ref\n ) => {\n // Color variant styles\n const variantClasses: Record<ColorVariant, string> = {\n neutral: 'text-neutral hover:text-neutral-hover active:text-neutral-active',\n primary: 'text-primary hover:text-primary-hover active:text-primary-active',\n accent: 'text-accent hover:text-accent-hover active:text-accent-active',\n info: 'text-info hover:text-info-hover active:text-info-active',\n success: 'text-success hover:text-success-hover active:text-success-active',\n warning: 'text-warning hover:text-warning-hover active:text-warning-active',\n error: 'text-error hover:text-error-hover active:text-error-active'\n };\n\n // Base classes - minimal styling, inline with text\n const baseClasses = [\n 'inline-flex items-center gap-0.5',\n 'font-medium',\n 'transition-colors duration-200',\n 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',\n disabled || loading ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',\n underline ? 'hover:underline' : '',\n variantClasses[
|
|
1
|
+
{"version":3,"file":"inline-button.js","sources":["../../../../src/components/forms/button/inline-button.tsx"],"sourcesContent":["import React, { useState } from 'react';\nimport { ColorVariant } from '../../../theme/tokens';\nimport { Icon } from '../../system/icon/icon';\n\nexport type InlineButtonProps = {\n /** Button label content - typically text */\n children: React.ReactNode;\n /** Click event handler */\n onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;\n /** Accessible label (optional, children used by default) */\n ariaLabel?: string;\n /** Disables button interaction and applies disabled styling */\n disabled?: boolean;\n /** HTML button type attribute */\n type?: 'button' | 'submit' | 'reset';\n /** Additional CSS classes applied to the button */\n className?: string;\n /** Icon name string passed to the Icon component to display alongside button text */\n icon?: string;\n /** Color variant - defaults to primary for link-like appearance */\n variant?: ColorVariant;\n /** Shows loading spinner and disables interaction */\n loading?: boolean;\n /** Whether to show underline on hover (default: true) */\n underline?: boolean;\n /** Icon size token - defaults to 'xs' to match text size */\n iconSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n /** Color variant to use when the button is hovered - overrides `variant` on hover */\n hoverVariant?: ColorVariant;\n};\n\n/**\n * Inline Button - a minimal button designed to be embedded within text\n *\n * Perfect for use cases like:\n * - \"By signing in, you agree to our [Terms of Service]\"\n * - \"Learn more about [pricing]\"\n * - Inline actions within paragraphs\n *\n * Features:\n * - No background or padding by default\n * - Inherits surrounding text sizing\n * - Optional icon support\n * - Color variants for different contexts\n * - Hover underline (can be disabled)\n * - Fully accessible\n */\nconst InlineButton = React.forwardRef<HTMLButtonElement, InlineButtonProps>(\n (\n {\n children,\n onClick,\n ariaLabel,\n disabled = false,\n type = 'button',\n className = '',\n icon,\n variant = 'primary',\n loading = false,\n underline = true,\n iconSize = 'xs',\n hoverVariant\n },\n ref\n ) => {\n const [isHovered, setIsHovered] = useState(false);\n const effectiveVariant = hoverVariant && isHovered ? hoverVariant : variant;\n\n // Color variant styles\n const variantClasses: Record<ColorVariant, string> = {\n neutral: 'text-neutral hover:text-neutral-hover active:text-neutral-active',\n primary: 'text-primary hover:text-primary-hover active:text-primary-active',\n accent: 'text-accent hover:text-accent-hover active:text-accent-active',\n info: 'text-info hover:text-info-hover active:text-info-active',\n success: 'text-success hover:text-success-hover active:text-success-active',\n warning: 'text-warning hover:text-warning-hover active:text-warning-active',\n error: 'text-error hover:text-error-hover active:text-error-active'\n };\n\n // Base classes - minimal styling, inline with text\n const baseClasses = [\n 'inline-flex items-center gap-0.5',\n 'font-medium',\n 'transition-colors duration-200',\n 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',\n disabled || loading ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',\n underline ? 'hover:underline' : '',\n variantClasses[effectiveVariant],\n className\n ]\n .filter(Boolean)\n .join(' ');\n\n // Clone icon and apply size to make it proportional to text\n const iconElement = icon ? <Icon name={icon} size={iconSize} /> : null;\n\n return (\n <button\n ref={ref}\n type={type}\n onClick={onClick}\n onMouseEnter={hoverVariant ? () => setIsHovered(true) : undefined}\n onMouseLeave={hoverVariant ? () => setIsHovered(false) : undefined}\n disabled={disabled || loading}\n aria-label={ariaLabel}\n className={baseClasses}\n >\n {children}\n {iconElement}\n </button>\n );\n }\n);\n\nInlineButton.displayName = 'InlineButton';\n\nexport default InlineButton;\n"],"names":[],"mappings":";;;AA+CA,MAAM,eAAe,MAAM;AAAA,EACzB,CACE;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,OAAO;AAAA,IACP,YAAY;AAAA,IACZ;AAAA,IACA,UAAU;AAAA,IACV,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,WAAW;AAAA,IACX;AAAA,EAAA,GAEF,QACG;AACH,UAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,UAAM,mBAAmB,gBAAgB,YAAY,eAAe;AAGpE,UAAM,iBAA+C;AAAA,MACnD,SAAS;AAAA,MACT,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,SAAS;AAAA,MACT,SAAS;AAAA,MACT,OAAO;AAAA,IAAA;AAIT,UAAM,cAAc;AAAA,MAClB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,YAAY,UAAU,kCAAkC;AAAA,MACxD,YAAY,oBAAoB;AAAA,MAChC,eAAe,gBAAgB;AAAA,MAC/B;AAAA,IAAA,EAEC,OAAO,OAAO,EACd,KAAK,GAAG;AAGX,UAAM,cAAc,OAAO,oBAAC,MAAA,EAAK,MAAM,MAAM,MAAM,UAAU,IAAK;AAElE,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA,cAAc,eAAe,MAAM,aAAa,IAAI,IAAI;AAAA,QACxD,cAAc,eAAe,MAAM,aAAa,KAAK,IAAI;AAAA,QACzD,UAAU,YAAY;AAAA,QACtB,cAAY;AAAA,QACZ,WAAW;AAAA,QAEV,UAAA;AAAA,UAAA;AAAA,UACA;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AACF;AAEA,aAAa,cAAc;"}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { ReactNode, MouseEvent } from 'react';
|
|
2
|
-
import { Size } from '../../../theme/size-tokens';
|
|
3
2
|
export type LeftNavItemProps = {
|
|
4
3
|
/** Icon identifier string to display (uses Icon component) */
|
|
5
4
|
icon?: string;
|
|
6
|
-
/**
|
|
7
|
-
iconSize?: Size;
|
|
8
|
-
/** Link label text (visible when expanded, hidden when collapsed) */
|
|
5
|
+
/** Link label text */
|
|
9
6
|
children: ReactNode;
|
|
10
7
|
/** Whether this item is currently active (highlighted state) */
|
|
11
8
|
active?: boolean;
|
|
@@ -15,7 +12,7 @@ export type LeftNavItemProps = {
|
|
|
15
12
|
badge?: ReactNode;
|
|
16
13
|
/** Destination URL for the navigation link (required for link mode) */
|
|
17
14
|
href?: string;
|
|
18
|
-
/** Accessible label override
|
|
15
|
+
/** Accessible label override */
|
|
19
16
|
title?: string;
|
|
20
17
|
/** Prevent actual navigation for demo/showcase mode (deprecated: use onClick instead) */
|
|
21
18
|
preventNavigation?: boolean;
|
|
@@ -48,7 +45,7 @@ export type LeftNavItemProps = {
|
|
|
48
45
|
* </LeftNavItem>
|
|
49
46
|
* ```
|
|
50
47
|
*/
|
|
51
|
-
declare function LeftNavItem({ icon,
|
|
48
|
+
declare function LeftNavItem({ icon, children, active, className, badge, href, title, preventNavigation, onClick, disableActiveState }: Readonly<LeftNavItemProps>): import("react/jsx-runtime").JSX.Element | null;
|
|
52
49
|
declare namespace LeftNavItem {
|
|
53
50
|
var displayName: string;
|
|
54
51
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"left-nav-item.d.ts","sourceRoot":"","sources":["../../../../src/components/layout/left-nav-layout/left-nav-item.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"left-nav-item.d.ts","sourceRoot":"","sources":["../../../../src/components/layout/left-nav-layout/left-nav-item.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAwC,MAAM,OAAO,CAAC;AAGpF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sBAAsB;IACtB,QAAQ,EAAE,SAAS,CAAC;IACpB,gEAAgE;IAChE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4DAA4D;IAC5D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yFAAyF;IACzF,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,uEAAuE;IACvE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,iBAAiB,GAAG,iBAAiB,CAAC,KAAK,IAAI,CAAC;IAC7E,8FAA8F;IAC9F,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,iBAAS,WAAW,CAAC,EACnB,IAAI,EACJ,QAAQ,EACR,MAAc,EACd,SAAc,EACd,KAAK,EACL,IAAI,EACJ,KAAK,EACL,iBAAyB,EACzB,OAAO,EACP,kBAA0B,EAC3B,EAAE,QAAQ,CAAC,gBAAgB,CAAC,kDAuF5B;kBAlGQ,WAAW;;;AAsGpB,eAAe,WAAW,CAAC"}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Icon } from "../../system/icon/icon.js";
|
|
3
|
-
import { useRef,
|
|
3
|
+
import { useRef, isValidElement, cloneElement } from "react";
|
|
4
4
|
import { NavLink } from "react-router-dom";
|
|
5
|
-
import Tooltip from "../../feedback/tooltip/tooltip.js";
|
|
6
5
|
function LeftNavItem({
|
|
7
6
|
icon,
|
|
8
|
-
iconSize = "sm",
|
|
9
7
|
children,
|
|
10
8
|
active = false,
|
|
11
9
|
className = "",
|
|
@@ -17,40 +15,25 @@ function LeftNavItem({
|
|
|
17
15
|
disableActiveState = false
|
|
18
16
|
}) {
|
|
19
17
|
const navRef = useRef(null);
|
|
20
|
-
const [isCollapsed, setIsCollapsed] = useState(() => {
|
|
21
|
-
if (typeof window !== "undefined") {
|
|
22
|
-
const navElement = document.querySelector("nav[data-collapsed]");
|
|
23
|
-
return navElement?.getAttribute("data-collapsed") === "true";
|
|
24
|
-
}
|
|
25
|
-
return false;
|
|
26
|
-
});
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
const checkCollapsed = () => {
|
|
29
|
-
const navElement2 = navRef.current?.closest("nav");
|
|
30
|
-
if (navElement2) {
|
|
31
|
-
setIsCollapsed(navElement2.getAttribute("data-collapsed") === "true");
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
checkCollapsed();
|
|
35
|
-
const navElement = navRef.current?.closest("nav");
|
|
36
|
-
if (navElement) {
|
|
37
|
-
const observer = new MutationObserver(checkCollapsed);
|
|
38
|
-
observer.observe(navElement, {
|
|
39
|
-
attributes: true,
|
|
40
|
-
attributeFilter: ["data-collapsed"]
|
|
41
|
-
});
|
|
42
|
-
return () => observer.disconnect();
|
|
43
|
-
}
|
|
44
|
-
return void 0;
|
|
45
|
-
}, []);
|
|
46
18
|
const itemTitle = title || (typeof children === "string" ? children : void 0);
|
|
47
19
|
const isButtonMode = !!onClick;
|
|
48
20
|
const isLinkMode = !isButtonMode && !!href;
|
|
49
|
-
const getBaseClasses = (isActive) => `group relative
|
|
21
|
+
const getBaseClasses = (isActive) => `group relative block w-full py-4 transition-colors ${!disableActiveState && (isActive || active) ? "bg-[#0C0E14] text-[#FBFBFF]" : "text-[#FBFBFF]/80 hover:text-[#FBFBFF] hover:bg-[#2e3148]"} ${className}`;
|
|
50
22
|
const renderContent = (isActive) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
23
|
+
!disableActiveState && (isActive || active) && /* @__PURE__ */ jsx(
|
|
24
|
+
"div",
|
|
25
|
+
{
|
|
26
|
+
className: "absolute left-0 top-0 bottom-0 w-0.75",
|
|
27
|
+
style: {
|
|
28
|
+
background: "radial-gradient(70.52% 85.16% at 0% 53.3%, #B44F8C 28.85%, #3E4BA5 100%)"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
),
|
|
32
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center gap-1.5 px-3", children: [
|
|
33
|
+
icon && /* @__PURE__ */ jsx(Icon, { name: icon, size: "md" }),
|
|
34
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold uppercase tracking-wide leading-none text-center select-none", children }),
|
|
35
|
+
badge && /* @__PURE__ */ jsx("span", { children: isValidElement(badge) && !disableActiveState && (isActive || active) ? cloneElement(badge, { inverted: true }) : badge })
|
|
36
|
+
] })
|
|
54
37
|
] });
|
|
55
38
|
const buttonElement = isButtonMode ? (
|
|
56
39
|
// eslint-disable-next-line jsx-a11y/anchor-is-valid
|
|
@@ -89,11 +72,7 @@ function LeftNavItem({
|
|
|
89
72
|
}
|
|
90
73
|
) : null;
|
|
91
74
|
const element = isButtonMode ? buttonElement : navLink;
|
|
92
|
-
|
|
93
|
-
return element;
|
|
94
|
-
}
|
|
95
|
-
const tooltipContent = typeof children === "string" ? children : itemTitle || "Menu Item";
|
|
96
|
-
return /* @__PURE__ */ jsx(Tooltip, { content: tooltipContent, position: "right", variant: "neutral", className: "w-fit", children: element });
|
|
75
|
+
return element;
|
|
97
76
|
}
|
|
98
77
|
LeftNavItem.displayName = "LeftNavItem";
|
|
99
78
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"left-nav-item.js","sources":["../../../../src/components/layout/left-nav-layout/left-nav-item.tsx"],"sourcesContent":["import Icon from '../../system/icon/icon';\nimport { ReactNode, MouseEvent, useRef,
|
|
1
|
+
{"version":3,"file":"left-nav-item.js","sources":["../../../../src/components/layout/left-nav-layout/left-nav-item.tsx"],"sourcesContent":["import Icon from '../../system/icon/icon';\nimport { ReactNode, MouseEvent, useRef, cloneElement, isValidElement } from 'react';\nimport { NavLink } from 'react-router-dom';\n\nexport type LeftNavItemProps = {\n /** Icon identifier string to display (uses Icon component) */\n icon?: string;\n /** Link label text */\n children: ReactNode;\n /** Whether this item is currently active (highlighted state) */\n active?: boolean;\n /** Additional CSS classes applied to the navigation link */\n className?: string;\n /** Badge or count component to display on the right side */\n badge?: ReactNode;\n /** Destination URL for the navigation link (required for link mode) */\n href?: string;\n /** Accessible label override */\n title?: string;\n /** Prevent actual navigation for demo/showcase mode (deprecated: use onClick instead) */\n preventNavigation?: boolean;\n /** Click handler - when provided, renders as button instead of link */\n onClick?: (event: MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;\n /** Disable active state highlighting (useful for non-navigation items like header buttons) */\n disableActiveState?: boolean;\n};\n\n/**\n * LeftNavItem - Navigation link or button item for LeftNavLayout\n *\n * Features:\n * - Polymorphic: renders as link (with href) or button (with onClick)\n * - Icon support with proper alignment\n * - Optional active state styling\n * - Tooltip on hover in collapsed mode (using Tooltip component with Portal)\n * - Badge/count support\n * - Accessible keyboard navigation\n *\n * @example\n * ```tsx\n * // Link mode\n * <LeftNavItem icon=\"home\" href=\"/\" active>\n * Home\n * </LeftNavItem>\n *\n * // Button mode\n * <LeftNavItem icon=\"settings\" onClick={() => console.log('clicked')} disableActiveState>\n * Settings\n * </LeftNavItem>\n * ```\n */\nfunction LeftNavItem({\n icon,\n children,\n active = false,\n className = '',\n badge,\n href,\n title,\n preventNavigation = false,\n onClick,\n disableActiveState = false\n}: Readonly<LeftNavItemProps>) {\n const navRef = useRef<HTMLAnchorElement | HTMLButtonElement>(null);\n\n // Use children as fallback title for accessibility\n const itemTitle = title || (typeof children === 'string' ? children : undefined);\n\n // Determine mode: button (onClick present) or link (href present)\n const isButtonMode = !!onClick;\n const isLinkMode = !isButtonMode && !!href;\n\n // Shared base classes for both button and link modes\n const getBaseClasses = (isActive: boolean) =>\n `group relative block w-full py-4 transition-colors ${\n !disableActiveState && (isActive || active)\n ? 'bg-[#0C0E14] text-[#FBFBFF]'\n : 'text-[#FBFBFF]/80 hover:text-[#FBFBFF] hover:bg-[#2e3148]'\n } ${className}`;\n\n // Shared content renderer\n const renderContent = (isActive: boolean) => (\n <>\n {/* Gradient left-border active indicator */}\n {!disableActiveState && (isActive || active) && (\n <div\n className=\"absolute left-0 top-0 bottom-0 w-0.75\"\n style={{\n background: 'radial-gradient(70.52% 85.16% at 0% 53.3%, #B44F8C 28.85%, #3E4BA5 100%)'\n }}\n />\n )}\n <div className=\"flex flex-col items-center justify-center gap-1.5 px-3\">\n {icon && <Icon name={icon} size=\"md\" />}\n <span className=\"text-[10px] font-semibold uppercase tracking-wide leading-none text-center select-none\">\n {children}\n </span>\n {badge && (\n <span>\n {isValidElement(badge) && !disableActiveState && (isActive || active)\n ? cloneElement(badge, { inverted: true } as { inverted: boolean })\n : badge}\n </span>\n )}\n </div>\n </>\n );\n\n // Button mode rendering\n const buttonElement = isButtonMode ? (\n // eslint-disable-next-line jsx-a11y/anchor-is-valid\n <a\n ref={navRef as React.RefObject<HTMLAnchorElement>}\n href=\"#\"\n onClick={(e) => {\n e.preventDefault();\n onClick?.(e as unknown as MouseEvent<HTMLAnchorElement>);\n }}\n className={getBaseClasses(active)}\n aria-current={!disableActiveState && active ? 'page' : undefined}\n aria-label={itemTitle}\n role=\"button\"\n >\n {renderContent(active)}\n </a>\n ) : null;\n\n // Link mode rendering\n const navLink = isLinkMode ? (\n <NavLink\n ref={navRef as React.RefObject<HTMLAnchorElement>}\n to={href!}\n onClick={(e: MouseEvent<HTMLAnchorElement>) => {\n if (preventNavigation) {\n e.preventDefault();\n }\n }}\n className={({ isActive }) => getBaseClasses(isActive)}\n aria-current={!disableActiveState && active ? 'page' : undefined}\n aria-label={itemTitle}\n end\n >\n {({ isActive }) => renderContent(isActive)}\n </NavLink>\n ) : null;\n\n const element = isButtonMode ? buttonElement : navLink;\n\n return element;\n}\n\nLeftNavItem.displayName = 'LeftNavItem';\n\nexport default LeftNavItem;\n"],"names":[],"mappings":";;;;AAmDA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA,oBAAoB;AAAA,EACpB;AAAA,EACA,qBAAqB;AACvB,GAA+B;AAC7B,QAAM,SAAS,OAA8C,IAAI;AAGjE,QAAM,YAAY,UAAU,OAAO,aAAa,WAAW,WAAW;AAGtE,QAAM,eAAe,CAAC,CAAC;AACvB,QAAM,aAAa,CAAC,gBAAgB,CAAC,CAAC;AAGtC,QAAM,iBAAiB,CAAC,aACtB,sDACE,CAAC,uBAAuB,YAAY,UAChC,gCACA,2DACN,IAAI,SAAS;AAGf,QAAM,gBAAgB,CAAC,aACrB,qBAAA,UAAA,EAEG,UAAA;AAAA,IAAA,CAAC,uBAAuB,YAAY,WACnC;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAU;AAAA,QACV,OAAO;AAAA,UACL,YAAY;AAAA,QAAA;AAAA,MACd;AAAA,IAAA;AAAA,IAGJ,qBAAC,OAAA,EAAI,WAAU,0DACZ,UAAA;AAAA,MAAA,QAAQ,oBAAC,MAAA,EAAK,MAAM,MAAM,MAAK,MAAK;AAAA,MACrC,oBAAC,QAAA,EAAK,WAAU,0FACb,SAAA,CACH;AAAA,MACC,SACC,oBAAC,QAAA,EACE,UAAA,eAAe,KAAK,KAAK,CAAC,uBAAuB,YAAY,UAC1D,aAAa,OAAO,EAAE,UAAU,KAAA,CAA+B,IAC/D,MAAA,CACN;AAAA,IAAA,EAAA,CAEJ;AAAA,EAAA,GACF;AAIF,QAAM,gBAAgB;AAAA;AAAA,IAEpB;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,KAAK;AAAA,QACL,MAAK;AAAA,QACL,SAAS,CAAC,MAAM;AACd,YAAE,eAAA;AACF,oBAAU,CAA6C;AAAA,QACzD;AAAA,QACA,WAAW,eAAe,MAAM;AAAA,QAChC,gBAAc,CAAC,sBAAsB,SAAS,SAAS;AAAA,QACvD,cAAY;AAAA,QACZ,MAAK;AAAA,QAEJ,wBAAc,MAAM;AAAA,MAAA;AAAA,IAAA;AAAA,MAErB;AAGJ,QAAM,UAAU,aACd;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAK;AAAA,MACL,IAAI;AAAA,MACJ,SAAS,CAAC,MAAqC;AAC7C,YAAI,mBAAmB;AACrB,YAAE,eAAA;AAAA,QACJ;AAAA,MACF;AAAA,MACA,WAAW,CAAC,EAAE,eAAe,eAAe,QAAQ;AAAA,MACpD,gBAAc,CAAC,sBAAsB,SAAS,SAAS;AAAA,MACvD,cAAY;AAAA,MACZ,KAAG;AAAA,MAEF,UAAA,CAAC,EAAE,SAAA,MAAe,cAAc,QAAQ;AAAA,IAAA;AAAA,EAAA,IAEzC;AAEJ,QAAM,UAAU,eAAe,gBAAgB;AAE/C,SAAO;AACT;AAEA,YAAY,cAAc;"}
|