@knymbus/voxel-ui 1.0.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.
Files changed (115) hide show
  1. package/.storybook/main.ts +17 -0
  2. package/.storybook/preview.ts +23 -0
  3. package/debug-storybook.log +40 -0
  4. package/dist/chunks/jsx-runtime-Boo2vksn.js +182 -0
  5. package/dist/chunks/resizable-ImB8dfG_.js +112 -0
  6. package/dist/chunks/tabs-MaVN00hJ.js +86 -0
  7. package/dist/components/button/Button.d.ts +31 -0
  8. package/dist/components/button/ButtonGroup.d.ts +12 -0
  9. package/dist/components/button/SplitActionButton.d.ts +2 -0
  10. package/dist/components/button/index.d.ts +5 -0
  11. package/dist/components/button/split-types.d.ts +16 -0
  12. package/dist/components/button/types.d.ts +9 -0
  13. package/dist/components/icons/AddIcon.d.ts +2 -0
  14. package/dist/components/icons/BlankDocIcon.d.ts +2 -0
  15. package/dist/components/icons/ChatIcon.d.ts +2 -0
  16. package/dist/components/icons/ChevronDownIcon.d.ts +2 -0
  17. package/dist/components/icons/CloseIcon.d.ts +2 -0
  18. package/dist/components/icons/CommentIcon.d.ts +2 -0
  19. package/dist/components/icons/DeleteChatIcon.d.ts +2 -0
  20. package/dist/components/icons/DocumentIcon.d.ts +2 -0
  21. package/dist/components/icons/ExpandIcon.d.ts +2 -0
  22. package/dist/components/icons/FolderIcon.d.ts +2 -0
  23. package/dist/components/icons/GroupIcon.d.ts +2 -0
  24. package/dist/components/icons/MinimizeIcon.d.ts +2 -0
  25. package/dist/components/icons/MinusIcon.d.ts +2 -0
  26. package/dist/components/icons/MoreIcon.d.ts +2 -0
  27. package/dist/components/icons/OpenFolderIcon.d.ts +2 -0
  28. package/dist/components/icons/PersonIcon.d.ts +2 -0
  29. package/dist/components/icons/PlusChatIcon.d.ts +2 -0
  30. package/dist/components/icons/PlusCommentIcon.d.ts +2 -0
  31. package/dist/components/icons/PlusDocBadgeIcon.d.ts +8 -0
  32. package/dist/components/icons/PlusDocIcon.d.ts +2 -0
  33. package/dist/components/icons/PlusPersonIcon.d.ts +2 -0
  34. package/dist/components/icons/RefreshIcon.d.ts +2 -0
  35. package/dist/components/icons/SearchIcon.d.ts +2 -0
  36. package/dist/components/icons/TerminalIcon.d.ts +2 -0
  37. package/dist/components/icons/TrashIcon.d.ts +2 -0
  38. package/dist/components/icons/TruckIcon.d.ts +2 -0
  39. package/dist/components/icons/index.d.ts +26 -0
  40. package/dist/components/icons/types.d.ts +5 -0
  41. package/dist/components/resizable/ResizablePanel.d.ts +10 -0
  42. package/dist/components/resizable/index.d.ts +1 -0
  43. package/dist/components/resizable/index.js +2 -0
  44. package/dist/components/search/SearchInput.d.ts +10 -0
  45. package/dist/components/search/index.d.ts +2 -0
  46. package/dist/components/search/types.d.ts +19 -0
  47. package/dist/components/tabs/TabButton.d.ts +12 -0
  48. package/dist/components/tabs/TabButtonGroup.d.ts +9 -0
  49. package/dist/components/tabs/TabPanel.d.ts +8 -0
  50. package/dist/components/tabs/TabPanelList.d.ts +11 -0
  51. package/dist/components/tabs/index.d.ts +5 -0
  52. package/dist/components/tabs/index.js +2 -0
  53. package/dist/components/tabs/types.d.ts +9 -0
  54. package/dist/components/tabs/useTab.d.ts +5 -0
  55. package/dist/index.d.ts +5 -0
  56. package/dist/index.js +1071 -0
  57. package/package.json +68 -0
  58. package/src/components/button/Button.stories.tsx +70 -0
  59. package/src/components/button/Button.tsx +108 -0
  60. package/src/components/button/ButtonGroup.stories.tsx +63 -0
  61. package/src/components/button/ButtonGroup.tsx +62 -0
  62. package/src/components/button/SplitActionButton.tsx +116 -0
  63. package/src/components/button/SplitButton.stories.tsx +55 -0
  64. package/src/components/button/index.ts +7 -0
  65. package/src/components/button/split-types.ts +18 -0
  66. package/src/components/button/types.ts +10 -0
  67. package/src/components/icons/AddIcon.tsx +10 -0
  68. package/src/components/icons/BlankDocIcon.tsx +10 -0
  69. package/src/components/icons/ChatIcon.tsx +9 -0
  70. package/src/components/icons/ChevronDownIcon.tsx +9 -0
  71. package/src/components/icons/CloseIcon.tsx +23 -0
  72. package/src/components/icons/CommentIcon.tsx +9 -0
  73. package/src/components/icons/DeleteChatIcon.tsx +10 -0
  74. package/src/components/icons/DocumentIcon.tsx +13 -0
  75. package/src/components/icons/ExpandIcon.tsx +12 -0
  76. package/src/components/icons/FolderIcon.tsx +9 -0
  77. package/src/components/icons/GroupIcon.tsx +12 -0
  78. package/src/components/icons/Icon.stories.tsx +122 -0
  79. package/src/components/icons/MinimizeIcon.tsx +12 -0
  80. package/src/components/icons/MinusIcon.tsx +9 -0
  81. package/src/components/icons/MoreIcon.tsx +11 -0
  82. package/src/components/icons/OpenFolderIcon.tsx +37 -0
  83. package/src/components/icons/PersonIcon.tsx +10 -0
  84. package/src/components/icons/PlusChatIcon.tsx +11 -0
  85. package/src/components/icons/PlusCommentIcon.tsx +11 -0
  86. package/src/components/icons/PlusDocBadgeIcon.tsx +74 -0
  87. package/src/components/icons/PlusDocIcon.tsx +12 -0
  88. package/src/components/icons/PlusPersonIcon.tsx +12 -0
  89. package/src/components/icons/RefreshIcon.tsx +9 -0
  90. package/src/components/icons/SearchIcon.tsx +10 -0
  91. package/src/components/icons/TerminalIcon.tsx +11 -0
  92. package/src/components/icons/TrashIcon.tsx +12 -0
  93. package/src/components/icons/TruckIcon.tsx +12 -0
  94. package/src/components/icons/index.ts +26 -0
  95. package/src/components/icons/types.ts +6 -0
  96. package/src/components/resizable/ResizablePanel.tsx +183 -0
  97. package/src/components/resizable/index.ts +1 -0
  98. package/src/components/search/SearchInput.stories.tsx +91 -0
  99. package/src/components/search/SearchInput.tsx +254 -0
  100. package/src/components/search/index.ts +2 -0
  101. package/src/components/search/types.ts +21 -0
  102. package/src/components/tabs/TabButton.tsx +56 -0
  103. package/src/components/tabs/TabButtonGroup.tsx +82 -0
  104. package/src/components/tabs/TabPanel.tsx +44 -0
  105. package/src/components/tabs/TabPanelList.tsx +31 -0
  106. package/src/components/tabs/Tabs.stories.tsx +71 -0
  107. package/src/components/tabs/index.ts +5 -0
  108. package/src/components/tabs/types.ts +10 -0
  109. package/src/components/tabs/useTab.ts +33 -0
  110. package/src/index.css +35 -0
  111. package/src/index.ts +5 -0
  112. package/src/vite-env.d.ts +5 -0
  113. package/tsconfig.json +47 -0
  114. package/vite.config.ts +64 -0
  115. package/vitest.shims.d.ts +1 -0
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@knymbus/voxel-ui",
3
+ "version": "1.0.0",
4
+ "main": "./dist/index.js",
5
+ "module": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts"
11
+ },
12
+ "./tabs": {
13
+ "import": "./dist/components/tabs/index.js",
14
+ "types": "./dist/components/tabs/index.d.ts"
15
+ },
16
+ "./resizable": {
17
+ "import": "./dist/components/resizable/index.js",
18
+ "types": "./dist/components/resizable/index.d.ts"
19
+ },
20
+ "./button":{
21
+ "import": "./dist/components/button/index.js",
22
+ "types":"./dist/components/button/index.d.ts"
23
+ },
24
+ "./icons":{
25
+ "import": "./dist/components/icons/index.js",
26
+ "types":"./dist/components/icons/index.d.ts"
27
+ },
28
+ "./search":{
29
+ "import": "./dist/components/search/index.js",
30
+ "types":"./dist/components/search/index.d.ts"
31
+ }
32
+ },
33
+ "scripts": {
34
+ "build": "vite build && tsc --emitDeclarationOnly",
35
+ "storybook": "storybook dev -p 6006",
36
+ "build-storybook": "storybook build",
37
+ "prepare": "npm run build",
38
+ "prepublishOnly": "npm run build",
39
+ "publish": "npm publish --access public",
40
+ "publish:patch": "npm version patch && npm publish --access public",
41
+ "publish:minor": "npm version minor && npm publish --access public"
42
+ },
43
+ "peerDependencies": {
44
+ "react": "^19.2.7",
45
+ "react-dom": "^19.2.7",
46
+ "zustand": "^5.0.14"
47
+ },
48
+ "keywords": [],
49
+ "author": "Ovel Heslop <kayheslop@gmail.com>",
50
+ "license": "ISC",
51
+ "type": "commonjs",
52
+ "description": "",
53
+ "dependencies": {
54
+ "@storybook/addon-vitest": "^10.4.6",
55
+ "@storybook/react": "^10.4.6",
56
+ "@storybook/react-vite": "^10.4.6",
57
+ "@tailwindcss/vite": "^4.3.2",
58
+ "@types/node": "^26.0.1",
59
+ "@types/react": "^19.2.17",
60
+ "@vitejs/plugin-react": "^6.0.3",
61
+ "@vitest/browser-playwright": "^4.1.9",
62
+ "storybook": "^10.4.6",
63
+ "tailwindcss": "^4.3.2",
64
+ "typescript": "^6.0.3",
65
+ "vite": "^8.1.0",
66
+ "vite-plugin-dts": "^5.0.3"
67
+ }
68
+ }
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { Button } from './Button';
4
+ import { Close, Terminal } from '../icons'
5
+
6
+ // Mock Lucide-shaped functional layout components components interfaces
7
+ const TerminalIcon = (props: { className?: string }) => <svg className={props.className} fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>;
8
+ const TrashIcon = (props: { className?: string }) => <svg className={props.className} fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>;
9
+ const ChevronRight = (props: { className?: string }) => <svg className={props.className} fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" /></svg>;
10
+
11
+ const meta: Meta<typeof Button> = {
12
+ title: 'Inputs/VoxelButtonUnion',
13
+ component: Button,
14
+ parameters: {
15
+ layout: 'centered',
16
+ },
17
+ };
18
+ export default meta;
19
+
20
+ export const EnterpriseButtonShowcase: StoryObj<typeof Button> = {
21
+ render: () => (
22
+ <div className="p-6 bg-vsc-sidebar border border-vsc-border rounded-md space-y-6 max-w-2xl select-none text-vsc-text">
23
+
24
+ {/* Colors Section */}
25
+ <div className="space-y-2">
26
+ <p className="text-[10px] font-bold text-vsc-muted uppercase tracking-wider">Color Themes Mapping</p>
27
+ <div className="flex flex-wrap items-center gap-3">
28
+ <Button color="primary">Commit Data</Button>
29
+ <Button color="secondary">Secondary Box</Button>
30
+ <Button color="ghost">Ghost Actions</Button>
31
+ <Button color="danger" startIcon={TrashIcon}>Purge Log</Button>
32
+ <Button color="vsc" startIcon={Terminal}>Console</Button>
33
+ </div>
34
+ </div>
35
+
36
+ {/* Sizing Rows */}
37
+ <div className="space-y-2">
38
+ <p className="text-[10px] font-bold text-vsc-muted uppercase tracking-wider">Functional Scaling Size Matrix</p>
39
+ <div className="flex flex-wrap items-end gap-3">
40
+ <Button size="xs" color="primary">Extra Small (xs)</Button>
41
+ <Button size="sm" color="primary">Small (sm)</Button>
42
+ <Button size="md" color="primary">Default (md)</Button>
43
+ <Button size="lg" color="primary">Large (lg)</Button>
44
+ </div>
45
+ </div>
46
+
47
+ {/* Icon Only Variants (Union Type Safe) */}
48
+ <div className="space-y-2">
49
+ <p className="text-[10px] font-bold text-vsc-muted uppercase tracking-wider">Icon-Only Variants (Strictly Restricted Type Union)</p>
50
+ <div className="flex flex-wrap items-center gap-3">
51
+ <Button iconOnly={true} icon={Terminal} size="xs" color="vsc" />
52
+ <Button iconOnly={true} icon={Terminal} size="sm" color="secondary" />
53
+ <Button iconOnly={true} icon={Close} size="sm" color="secondary" />
54
+ <Button iconOnly={true} icon={TrashIcon} size="md" color="danger" />
55
+ <Button iconOnly={true} icon={ChevronRight} size="lg" color="primary" />
56
+ </div>
57
+ </div>
58
+
59
+ {/* Disabled States Verification */}
60
+ <div className="space-y-2">
61
+ <p className="text-[10px] font-bold text-vsc-muted uppercase tracking-wider">Disabled Guardrail Verification</p>
62
+ <div className="flex flex-wrap items-center gap-3">
63
+ <Button color="primary" disabled startIcon={Terminal}>Locked Commit</Button>
64
+ <Button iconOnly={true} icon={Terminal} size="md" color="secondary" disabled />
65
+ </div>
66
+ </div>
67
+
68
+ </div>
69
+ ),
70
+ };
@@ -0,0 +1,108 @@
1
+ import React, { forwardRef } from 'react';
2
+
3
+ // Define specialized type contract templates for our Lucide icon objects interfaces
4
+ export type LucideIcon = React.ComponentType<{ className?: string }>;
5
+
6
+ export type ButtonColor = "primary" | "secondary" | "ghost" | "danger" | "vsc";
7
+ export type ButtonSize = "xs" | "sm" | "md" | "lg";
8
+
9
+ // 1. Shared icon size map exposed for standalone use across the library
10
+ export const buttonIconSizes: Record<ButtonSize, string> = {
11
+ xs: "w-3 h-3",
12
+ sm: "w-3.5 h-3.5",
13
+ md: "w-4 h-4",
14
+ lg: "w-5 h-5",
15
+ };
16
+
17
+ interface ButtonIconProps extends React.SVGProps<SVGSVGElement> {
18
+ icon: LucideIcon;
19
+ size?: ButtonSize;
20
+ }
21
+
22
+ // 2. Standalone ButtonIcon component UI
23
+ export const ButtonIcon = ({ icon: Icon, size = "sm", className = "", ...props }: ButtonIconProps) => {
24
+ const sizeClass = buttonIconSizes[size];
25
+ return <Icon className={`${sizeClass} ${className}`.trim()} {...props} />;
26
+ };
27
+
28
+ interface BaseButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
29
+ size?: ButtonSize;
30
+ color?: ButtonColor;
31
+ startIcon?: LucideIcon;
32
+ endIcon?: LucideIcon;
33
+ }
34
+
35
+ interface IconButtonProps extends BaseButtonProps {
36
+ iconOnly: true;
37
+ icon: LucideIcon;
38
+ children?: never; // Explicitly compile-ban children text fields during icon-only layouts
39
+ }
40
+
41
+ interface StandardButtonProps extends BaseButtonProps {
42
+ iconOnly?: false;
43
+ icon?: never;
44
+ children: React.ReactNode;
45
+ }
46
+
47
+ export type ButtonProps = IconButtonProps | StandardButtonProps;
48
+
49
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
50
+ const {
51
+ size = "sm",
52
+ color = "vsc",
53
+ iconOnly = false,
54
+ startIcon,
55
+ endIcon,
56
+ className = "",
57
+ disabled,
58
+ ...rest
59
+ } = props;
60
+
61
+ // Harmonized styling sheets mapped to your Tailwind v4 VS Code variable design tokens
62
+ const colorStyles: Record<ButtonColor, string> = {
63
+ primary: "bg-vsc-accent text-vsc-button-text hover:bg-vsc-accent-hover border border-transparent focus:border-vsc-accent/50",
64
+ secondary: "bg-vsc-bg border border-vsc-border text-vsc-text hover:bg-vsc-hover focus:border-vsc-accent",
65
+ ghost: "bg-transparent border border-transparent text-vsc-muted hover:bg-vsc-hover/40 hover:text-vsc-text focus:border-vsc-border",
66
+ danger: "bg-rose-500/10 border border-rose-500/20 text-rose-500 hover:bg-rose-500/20 focus:border-rose-500/50",
67
+ vsc: "bg-transparent text-vsc-text border border-transparent opacity-55 hover:opacity-100 hover:bg-vsc-hover focus:border-vsc-hover/50",
68
+ };
69
+
70
+ const sizeStyles: Record<ButtonSize, string> = {
71
+ xs: iconOnly ? "p-1 rounded-sm h-5 w-5" : "px-2 py-0.5 text-[10px] rounded-sm gap-1 h-5.5",
72
+ sm: iconOnly ? "p-1.5 rounded-md h-7 w-7" : "px-3 py-1.5 text-xs rounded-md gap-1.5 h-7.5",
73
+ md: iconOnly ? "p-2 rounded-md h-9 w-9" : "px-4 py-2 text-sm rounded-md gap-2 h-9",
74
+ lg: iconOnly ? "p-3 rounded-lg h-11 w-11" : "px-5 py-2.5 text-base rounded-lg gap-2.5 h-11",
75
+ };
76
+
77
+ const baseClasses = `
78
+ inline-flex items-center justify-center font-sans font-medium select-none
79
+ outline-none focus:ring-0 cursor-pointer
80
+ disabled:opacity-40 disabled:cursor-not-allowed disabled:transform-none disabled:active:scale-100 disabled:pointer-events-none
81
+ transition-all duration-150 cubic-bezier(0.34, 1.56, 0.64, 1)
82
+ active:scale-95 active:duration-75
83
+ `.trim();
84
+
85
+ const dynamicClasses = `${baseClasses} ${colorStyles[color]} ${sizeStyles[size]} ${className}`;
86
+
87
+ if (iconOnly) {
88
+ const iconOnlyElement = (props as IconButtonProps).icon;
89
+ return (
90
+ <button ref={ref} disabled={disabled} className={dynamicClasses} {...rest}>
91
+ <ButtonIcon icon={iconOnlyElement} size={size} />
92
+ </button>
93
+ );
94
+ }
95
+
96
+ return (
97
+ <button ref={ref} disabled={disabled} className={dynamicClasses} {...rest}>
98
+ {startIcon && <ButtonIcon icon={startIcon} size={size} />}
99
+ <span>{props.children}</span>
100
+ {endIcon && <ButtonIcon icon={endIcon} size={size} />}
101
+ </button>
102
+ );
103
+ });
104
+
105
+ Button.displayName = "Button";
106
+
107
+
108
+ export default Button
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import ButtonGroup from './ButtonGroup';
4
+ import { Button } from './Button';
5
+
6
+ // Mock icons
7
+ const PlayIcon = (props: { className?: string }) => <svg className={props.className} fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path strokeLinecap="round" strokeLinejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>;
8
+ const DebugIcon = (props: { className?: string }) => <svg className={props.className} fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" /></svg>;
9
+ const SettingsIcon = (props: { className?: string }) => <svg className={props.className} fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>;
10
+ const ChevronDown = (props: { className?: string }) => (
11
+ <svg className={props.className} fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
12
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
13
+ </svg>
14
+ );
15
+
16
+ const meta: Meta<typeof ButtonGroup> = {
17
+ title: 'Inputs/VoxelButtonGroupAdvanced',
18
+ component: ButtonGroup,
19
+ parameters: {
20
+ layout: 'centered',
21
+ },
22
+ };
23
+ export default meta;
24
+
25
+ export const AdvancedGroupSandbox: StoryObj<typeof ButtonGroup> = {
26
+ render: () => (
27
+ <div className="p-6 bg-vsc-bg border border-vsc-border rounded-md space-y-6 w-96 text-vsc-text">
28
+
29
+ {/* 1. High-Fidelity Floating Ghost Toolbar (No outer borders, clean margins) */}
30
+ <div className="space-y-1.5">
31
+ <p className="text-[10px] font-bold text-vsc-muted uppercase tracking-wider">VS Code Style Floating Ghost Tray</p>
32
+ <div className="flex items-center justify-between bg-vsc-sidebar border border-vsc-border p-1 rounded-sm w-full">
33
+ <span className="text-xs font-mono text-vsc-muted pl-2">terminal.log</span>
34
+ <ButtonGroup variant="ghost" orientation="horizontal">
35
+ <Button iconOnly icon={PlayIcon} size="xs" title="Run Execution Pipeline" />
36
+ <Button iconOnly icon={DebugIcon} size="xs" title="Toggle Breakpoint Debugger" />
37
+ <Button iconOnly icon={SettingsIcon} size="xs" title="Configure Console Prefs" />
38
+ </ButtonGroup>
39
+ </div>
40
+ </div>
41
+
42
+ {/* 2. Custom Padding Spacing Gap Layout (Horizontal) */}
43
+ <div className="space-y-1.5">
44
+ <p className="text-[10px] font-bold text-vsc-muted uppercase tracking-wider">Horizontal Layout with 8px Padding Gap</p>
45
+ <ButtonGroup orientation="horizontal" gap={8}>
46
+ <Button color="primary" size="sm">Execute</Button>
47
+ <Button color="secondary" size="sm">Hold</Button>
48
+ <Button color="danger" size="sm">Abort</Button>
49
+ </ButtonGroup>
50
+ </div>
51
+
52
+ {/* 3. Custom Padding Spacing Gap Layout (Vertical) */}
53
+ <div className="space-y-1.5">
54
+ <p className="text-[10px] font-bold text-vsc-muted uppercase tracking-wider">Vertical Layout with 6px Padding Gap</p>
55
+ <ButtonGroup orientation="vertical" gap={6}>
56
+ <Button color="secondary" size="sm" startIcon={PlayIcon}>Initialize Channel</Button>
57
+ <Button color="secondary" size="sm" startIcon={SettingsIcon}>Hardware Override</Button>
58
+ </ButtonGroup>
59
+ </div>
60
+
61
+ </div>
62
+ ),
63
+ };
@@ -0,0 +1,62 @@
1
+ import React from 'react';
2
+
3
+ export type GroupOrientation = 'horizontal' | 'vertical';
4
+ export type GroupVariant = 'default' | 'ghost';
5
+
6
+ interface ButtonGroupProps {
7
+ children: React.ReactNode;
8
+ orientation?: GroupOrientation;
9
+ variant?: GroupVariant;
10
+ gap?: number; // Custom distance padding spacing metric in pixels
11
+ className?: string;
12
+ }
13
+
14
+ export default function ButtonGroup({
15
+ children,
16
+ orientation = 'horizontal',
17
+ variant = 'default',
18
+ gap = 0,
19
+ className = ''
20
+ }: ButtonGroupProps) {
21
+
22
+ const baseClasses = "inline-flex bg-transparent rounded-sm select-none isolation-auto";
23
+ const isHorizontal = orientation === 'horizontal';
24
+ const hasGap = gap > 0;
25
+
26
+ // 1. Configure the core structural orientation layout flow axis
27
+ const orientationClass = isHorizontal ? 'flex-row' : 'flex-col';
28
+
29
+ // 2. Configure dense overlapping border adjustments only if elements are touching (gap === 0)
30
+ let structuralClasses = '';
31
+ if (!hasGap && variant === 'default') {
32
+ structuralClasses = isHorizontal
33
+ ? '[&>button:not(:first-child)]:-ml-[1px] [&>button:first-child]:rounded-l-sm [&>button:last-child]:rounded-r-sm [&>button:not(:first-child):not(:last-child)]:rounded-none'
34
+ : '[&>button:not(:first-child)]:-mt-[1px] [&>button:first-child]:rounded-t-sm [&>button:last-child]:rounded-b-sm [&>button:not(:first-child):not(:last-child)]:rounded-none';
35
+ } else if (hasGap) {
36
+ // Gracefully preserve default corner curves if items use independent grid spacing margins
37
+ structuralClasses = '[&>button]:rounded-sm';
38
+ }
39
+
40
+ // 3. Dynamically intercept sub-children buttons style attributes if variant is set to ghost
41
+ const processedChildren = React.Children.map(children, (child) => {
42
+ if (React.isValidElement(child) && variant === 'ghost') {
43
+ // Force individual child button components to drop colors and adopt ghost profiles implicitly
44
+ return React.cloneElement(child as React.ReactElement<any>, { color: 'ghost' });
45
+ }
46
+ return child;
47
+ });
48
+
49
+ // Calculate pixel spacing overrides using native inline CSS variables parameters
50
+ const inlineStyles: React.CSSProperties = hasGap ? {
51
+ gap: `${gap}px`
52
+ } : {};
53
+
54
+ return (
55
+ <div
56
+ className={`${baseClasses} ${orientationClass} ${structuralClasses} ${className}`.trim()}
57
+ style={inlineStyles}
58
+ >
59
+ {processedChildren}
60
+ </div>
61
+ );
62
+ }
@@ -0,0 +1,116 @@
1
+ import { useState, useRef, useEffect } from 'react';
2
+ import { Button, ButtonIcon } from './Button';
3
+ import { SplitActionButtonProps } from './split-types';
4
+
5
+ // Simple inner ChevronDown vector icon utility to match your signature
6
+ const ChevronDownIcon = (props: { className?: string }) => (
7
+ <svg className={props.className} fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24">
8
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
9
+ </svg>
10
+ );
11
+
12
+ export default function SplitActionButton({
13
+ children,
14
+ color = 'primary',
15
+ size = 'sm',
16
+ startIcon,
17
+ onMainActionClick,
18
+ menuItems,
19
+ className = ''
20
+ }: SplitActionButtonProps) {
21
+ const [isOpen, setIsOpen] = useState<boolean>(false);
22
+ const containerRef = useRef<HTMLDivElement | null>(null);
23
+
24
+ // Auto-close menu drawer container panel if agent clicks outside the node frame
25
+ useEffect(() => {
26
+ const handleOutsideClick = (e: MouseEvent) => {
27
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
28
+ setIsOpen(false);
29
+ }
30
+ };
31
+ if (isOpen) window.addEventListener('mousedown', handleOutsideClick);
32
+ return () => window.removeEventListener('mousedown', handleOutsideClick);
33
+ }, [isOpen]);
34
+
35
+ // High-fidelity sizing height-matching configuration maps overrides
36
+ const heightStyles = {
37
+ xs: { height: '22px', menuTop: '26px' },
38
+ sm: { height: '30px', menuTop: '34px' },
39
+ md: { height: '36px', menuTop: '40px' },
40
+ lg: { height: '44px', menuTop: '48px' }
41
+ }[size];
42
+
43
+ // Separator divider line colors dependent upon active color tokens
44
+ const separatorColor = color === 'primary'
45
+ ? 'bg-vsc-accent-hover/60'
46
+ : 'bg-vsc-border';
47
+
48
+ return (
49
+ <div
50
+ ref={containerRef}
51
+ className={`relative inline-flex items-center rounded-sm select-none isolation-auto ${className}`.trim()}
52
+ style={{ height: heightStyles.height }}
53
+ >
54
+ {/*
55
+ 1. Left Element: Primary Core Action Text Button.
56
+ Forces inner-right corners to flat 0 to resolve rounding gaps.
57
+ */}
58
+ <Button
59
+ color={color}
60
+ size={size}
61
+ startIcon={startIcon}
62
+ onClick={onMainActionClick}
63
+ className="rounded-r-none! h-full border-r-0"
64
+ >
65
+ {children}
66
+ </Button>
67
+
68
+ {/*
69
+ 2. Middle Element: High-Density Absolute Separator Dividing Line.
70
+ Bypasses border collisions cleanly.
71
+ */}
72
+ <div className={`w-px h-3/5 pointer-events-none z-20 ${separatorColor}`} />
73
+
74
+ {/*
75
+ 3. Right Element: Dropdown Arrow Icon Control Switch.
76
+ Forces inner-left corners to flat 0 to link cleanly to text block.
77
+ */}
78
+ <button
79
+ onClick={() => setIsOpen(!isOpen)}
80
+ className={`h-full flex items-center justify-center transition-all duration-150 focus:outline-none border-none outline-none rounded-l-none! cursor-pointer ${
81
+ color === 'primary'
82
+ ? 'bg-vsc-accent text-vsc-button-text hover:bg-vsc-accent-hover'
83
+ : color === 'secondary'
84
+ ? 'bg-vsc-bg border border-vsc-sidebar border-l-0 text-vsc-text hover:bg-vsc-hover'
85
+ : 'bg-transparent text-vsc-muted hover:bg-vsc-hover/40 hover:text-vsc-text'
86
+ } ${
87
+ size === 'xs' ? 'w-5 rounded-r-sm' : size === 'sm' ? 'w-7 rounded-r-md' : size === 'md' ? 'w-9 rounded-r-md' : 'w-11 rounded-r-lg'
88
+ }`}
89
+ >
90
+ <ChevronDownIcon className={`w-3.5 h-3.5 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} />
91
+ </button>
92
+
93
+ {/*
94
+ 4. Floating Options Drawer Menu Panel.
95
+ Renders absolutely using high z-index stacking rules.
96
+ */}
97
+ {isOpen && (
98
+ <div
99
+ className="absolute left-0 w-48 bg-vsc-sidebar border border-vsc-border rounded shadow-xl z-50 p-1 flex flex-col font-sans text-xs text-vsc-text animate-in fade-in slide-in-from-top-2 duration-150"
100
+ style={{ top: heightStyles.menuTop }}
101
+ >
102
+ {menuItems.map((item) => (
103
+ <button
104
+ key={item.id}
105
+ onClick={() => { item.onClick(); setIsOpen(false); }}
106
+ className="w-full flex items-center gap-2 p-2 hover:bg-vsc-hover text-left rounded-sm border-none bg-transparent text-vsc-text hover:text-white cursor-pointer"
107
+ >
108
+ {item.startIcon && <ButtonIcon icon={item.startIcon} size="xs" className="opacity-70" />}
109
+ <span className="truncate">{item.label}</span>
110
+ </button>
111
+ ))}
112
+ </div>
113
+ )}
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import SplitActionButton from './SplitActionButton';
4
+ import { SplitActionMenuItem } from './split-types';
5
+
6
+ const DocumentIcon = (props: { className?: string }) => <svg className={props.className} fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>;
7
+ const FolderIcon = (props: { className?: string }) => <svg className={props.className} fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>;
8
+
9
+ const meta: Meta<typeof SplitActionButton> = {
10
+ title: 'Inputs/VoxelSplitActionButton',
11
+ component: SplitActionButton,
12
+ parameters: {
13
+ layout: 'centered',
14
+ },
15
+ };
16
+ export default meta;
17
+
18
+ const SAMPLE_MENU_ITEMS: SplitActionMenuItem[] = [
19
+ { id: 'new_file', label: 'Create Flat text file (.txt)', startIcon: DocumentIcon, onClick: () => alert('New text file staged!') },
20
+ { id: 'new_folder', label: 'Create Directory node', startIcon: FolderIcon, onClick: () => alert('New folder tree compiled!') }
21
+ ];
22
+
23
+ export const SplitActionShowcaseSandbox: StoryObj<typeof SplitActionButton> = {
24
+ render: () => (
25
+ <div className="p-12 bg-vsc-sidebar border border-vsc-border rounded-md space-y-8 min-w-125 h-64 flex flex-col justify-start items-center select-none">
26
+
27
+ {/* Primary Split Menu (Vsc Accent Theme) */}
28
+ <div className="flex flex-col items-center gap-2">
29
+ <p className="text-[10px] font-bold text-vsc-muted uppercase tracking-wider">Primary Theme Split Profile</p>
30
+ <SplitActionButton
31
+ color="primary"
32
+ size="sm"
33
+ onMainActionClick={() => alert('Primary Action Executed: Generating Default Manifest CSV!')}
34
+ menuItems={SAMPLE_MENU_ITEMS}
35
+ >
36
+ New Manifest File
37
+ </SplitActionButton>
38
+ </div>
39
+
40
+ {/* Secondary Split Menu (Matte Border Toolbar Theme) */}
41
+ <div className="flex flex-col items-center gap-2">
42
+ <p className="text-[10px] font-bold text-vsc-muted uppercase tracking-wider">Secondary Matte Toolbar Split Profile</p>
43
+ <SplitActionButton
44
+ color="secondary"
45
+ size="md"
46
+ onMainActionClick={() => alert('Secondary Action Triggered!')}
47
+ menuItems={SAMPLE_MENU_ITEMS}
48
+ >
49
+ Options Explorer
50
+ </SplitActionButton>
51
+ </div>
52
+
53
+ </div>
54
+ ),
55
+ };
@@ -0,0 +1,7 @@
1
+ export { default as Button } from './Button';
2
+ export { default as ButtonGroup } from './ButtonGroup';
3
+ export { default as SplitActionButton } from './SplitActionButton';
4
+
5
+ // Types
6
+ export * from './split-types';
7
+ export * from './types';
@@ -0,0 +1,18 @@
1
+ import { LucideIcon, ButtonColor, ButtonSize } from './Button';
2
+
3
+ export interface SplitActionMenuItem {
4
+ id: string;
5
+ label: string;
6
+ startIcon?: LucideIcon;
7
+ onClick: () => void;
8
+ }
9
+
10
+ export interface SplitActionButtonProps {
11
+ children: React.ReactNode;
12
+ color?: ButtonColor;
13
+ size?: ButtonSize;
14
+ startIcon?: LucideIcon;
15
+ onMainActionClick: () => void;
16
+ menuItems: SplitActionMenuItem[];
17
+ className?: string;
18
+ }
@@ -0,0 +1,10 @@
1
+ export type ButtonVariant = 'primary' | 'secondary' | 'accent' | 'ghost' | 'danger';
2
+ export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg';
3
+
4
+ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
5
+ variant?: ButtonVariant;
6
+ size?: ButtonSize;
7
+ startIcon?: React.ReactNode;
8
+ endIcon?: React.ReactNode;
9
+ fullWidth?: boolean;
10
+ }
@@ -0,0 +1,10 @@
1
+ import { IconProps } from "./types";
2
+
3
+ export default function AddIcon({ size = 16, className = '', ...props }: IconProps) {
4
+ return (
5
+ <svg xmlns="http://w3.org" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className={`shrink-0 select-none ${className}`.trim()} {...props}>
6
+ <line x1="12" y1="5" x2="12" y2="19" />
7
+ <line x1="5" y1="12" x2="19" y2="12" />
8
+ </svg>
9
+ );
10
+ }
@@ -0,0 +1,10 @@
1
+ import { IconProps } from "./types";
2
+
3
+ export default function BlankDocIcon({ size = 16, className = '', ...props }: IconProps) {
4
+ return (
5
+ <svg xmlns="http://w3.org" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" className={`shrink-0 select-none ${className}`.trim()} {...props}>
6
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
7
+ <polyline points="14 2 14 8 20 8" />
8
+ </svg>
9
+ );
10
+ }
@@ -0,0 +1,9 @@
1
+ import { IconProps } from "./types";
2
+
3
+ export default function ChatIcon({ size = 16, className = '', ...props }: IconProps) {
4
+ return (
5
+ <svg xmlns="http://w3.org" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" className={`shrink-0 select-none ${className}`.trim()} {...props}>
6
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
7
+ </svg>
8
+ );
9
+ }
@@ -0,0 +1,9 @@
1
+ import { IconProps } from "./types";
2
+
3
+ export default function ChevronDownIcon({ size = 16, className = '', ...props }: IconProps) {
4
+ return (
5
+ <svg xmlns="http://w3.org" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className={`shrink-0 select-none ${className}`.trim()} {...props}>
6
+ <polyline points="6 9 12 15 18 9" />
7
+ </svg>
8
+ );
9
+ }
@@ -0,0 +1,23 @@
1
+
2
+ import { IconProps } from './types';
3
+
4
+ export default function CloseIcon({ size = 16, className = '', ...props }: IconProps) {
5
+ return (
6
+ <svg
7
+ xmlns="http://w3.org"
8
+ width={size}
9
+ height={size}
10
+ viewBox="0 0 24 24"
11
+ fill="none"
12
+ stroke="currentColor"
13
+ strokeWidth="2.5" // Thicker stroke weight to guarantee sharp visibility on high-DPI/Retina screens
14
+ strokeLinecap="round"
15
+ strokeLinejoin="round"
16
+ className={`shrink-0 select-none ${className}`.trim()}
17
+ {...props}
18
+ >
19
+ <line x1="18" y1="6" x2="6" y2="18" />
20
+ <line x1="6" y1="6" x2="18" y2="18" />
21
+ </svg>
22
+ );
23
+ }