radtools 0.1.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.
- package/README.md +108 -0
- package/bin/radtools.js +5 -0
- package/dist/cli/index.js +427 -0
- package/package.json +55 -0
- package/templates/api-routes/assets/optimize/route.ts +94 -0
- package/templates/api-routes/assets/route.ts +159 -0
- package/templates/api-routes/components/create-folder/route.ts +55 -0
- package/templates/api-routes/components/route.ts +156 -0
- package/templates/api-routes/fonts/route.ts +96 -0
- package/templates/api-routes/fonts/upload/route.ts +79 -0
- package/templates/api-routes/read-css/route.ts +29 -0
- package/templates/api-routes/write-css/route.ts +423 -0
- package/templates/components/Rad_os/AppWindow.tsx +423 -0
- package/templates/components/Rad_os/MobileAppModal.tsx +76 -0
- package/templates/components/Rad_os/WindowTitleBar.tsx +290 -0
- package/templates/components/icons/Icon.tsx +224 -0
- package/templates/components/icons/README.md +85 -0
- package/templates/components/icons/index.ts +20 -0
- package/templates/components/icons.tsx +164 -0
- package/templates/components/ui/Accordion.tsx +268 -0
- package/templates/components/ui/Alert.tsx +111 -0
- package/templates/components/ui/Badge.tsx +87 -0
- package/templates/components/ui/Breadcrumbs.tsx +88 -0
- package/templates/components/ui/Button.tsx +249 -0
- package/templates/components/ui/Card.tsx +137 -0
- package/templates/components/ui/Checkbox.tsx +137 -0
- package/templates/components/ui/ContextMenu.tsx +220 -0
- package/templates/components/ui/Dialog.tsx +264 -0
- package/templates/components/ui/Divider.tsx +70 -0
- package/templates/components/ui/DropdownMenu.tsx +301 -0
- package/templates/components/ui/HelpPanel.tsx +119 -0
- package/templates/components/ui/Input.tsx +176 -0
- package/templates/components/ui/Popover.tsx +211 -0
- package/templates/components/ui/Progress.tsx +158 -0
- package/templates/components/ui/Select.tsx +134 -0
- package/templates/components/ui/Sheet.tsx +316 -0
- package/templates/components/ui/Slider.tsx +223 -0
- package/templates/components/ui/Switch.tsx +155 -0
- package/templates/components/ui/Tabs.tsx +253 -0
- package/templates/components/ui/Toast.tsx +192 -0
- package/templates/components/ui/Tooltip.tsx +129 -0
- package/templates/components/ui/hooks/useModalBehavior.ts +66 -0
- package/templates/components/ui/index.ts +84 -0
- package/templates/devtools/DevToolsPanel.tsx +261 -0
- package/templates/devtools/DevToolsProvider.tsx +43 -0
- package/templates/devtools/components/BreakpointIndicator.tsx +49 -0
- package/templates/devtools/components/ColorPicker.tsx +33 -0
- package/templates/devtools/components/ComponentsSecondaryNav.tsx +44 -0
- package/templates/devtools/components/ContextualFooter.tsx +56 -0
- package/templates/devtools/components/DraggablePanel.tsx +43 -0
- package/templates/devtools/components/PrimaryNavigationFooter.tsx +254 -0
- package/templates/devtools/components/SearchableColorDropdown.tsx +253 -0
- package/templates/devtools/components/SecondaryNavigation.tsx +36 -0
- package/templates/devtools/components/TokenDropdown.tsx +47 -0
- package/templates/devtools/components/TypographyFooter.tsx +145 -0
- package/templates/devtools/hooks/useMockState.ts +16 -0
- package/templates/devtools/index.ts +17 -0
- package/templates/devtools/lib/componentScanner.ts +78 -0
- package/templates/devtools/lib/cssParser.ts +465 -0
- package/templates/devtools/lib/searchIndexes.ts +45 -0
- package/templates/devtools/lib/selectorGenerator.ts +86 -0
- package/templates/devtools/store/index.ts +66 -0
- package/templates/devtools/store/slices/assetsSlice.ts +106 -0
- package/templates/devtools/store/slices/componentsSlice.ts +59 -0
- package/templates/devtools/store/slices/mockStatesSlice.ts +77 -0
- package/templates/devtools/store/slices/panelSlice.ts +17 -0
- package/templates/devtools/store/slices/typographySlice.ts +538 -0
- package/templates/devtools/store/slices/variablesSlice.ts +167 -0
- package/templates/devtools/tabs/AssetsTab/AssetGrid.tsx +76 -0
- package/templates/devtools/tabs/AssetsTab/FolderTree.tsx +53 -0
- package/templates/devtools/tabs/AssetsTab/UploadDropzone.tsx +76 -0
- package/templates/devtools/tabs/AssetsTab/index.tsx +182 -0
- package/templates/devtools/tabs/ComponentsTab/AddTabButton.tsx +63 -0
- package/templates/devtools/tabs/ComponentsTab/ComponentList.tsx +153 -0
- package/templates/devtools/tabs/ComponentsTab/DesignSystemTab.tsx +1515 -0
- package/templates/devtools/tabs/ComponentsTab/DynamicFolderTab.tsx +113 -0
- package/templates/devtools/tabs/ComponentsTab/PropDisplay.tsx +55 -0
- package/templates/devtools/tabs/ComponentsTab/index.tsx +167 -0
- package/templates/devtools/tabs/ComponentsTab/previews/.gitkeep +4 -0
- package/templates/devtools/tabs/ComponentsTab/previews/Rad_os.tsx +262 -0
- package/templates/devtools/tabs/ComponentsTab/tabConfig.ts +53 -0
- package/templates/devtools/tabs/MockStatesTab/index.tsx +29 -0
- package/templates/devtools/tabs/TypographyTab/FontManager.tsx +421 -0
- package/templates/devtools/tabs/TypographyTab/TypographyStylesDisplay.tsx +290 -0
- package/templates/devtools/tabs/TypographyTab/index.tsx +98 -0
- package/templates/devtools/tabs/VariablesTab/BaseColorEditor.tsx +267 -0
- package/templates/devtools/tabs/VariablesTab/BorderRadiusEditor.tsx +37 -0
- package/templates/devtools/tabs/VariablesTab/ColorModeSelector.tsx +235 -0
- package/templates/devtools/tabs/VariablesTab/index.tsx +100 -0
- package/templates/devtools/types/index.ts +99 -0
- package/templates/globals.css +574 -0
- package/templates/hooks/index.ts +1 -0
- package/templates/hooks/useWindowManager.ts +212 -0
- package/templates/public/assets/icons/avatar.svg +18 -0
- package/templates/public/assets/icons/checkmark-filled.svg +14 -0
- package/templates/public/assets/icons/checkmark.svg +14 -0
- package/templates/public/assets/icons/chevron-down.svg +14 -0
- package/templates/public/assets/icons/close.svg +14 -0
- package/templates/public/assets/icons/copy.svg +14 -0
- package/templates/public/assets/icons/download.svg +14 -0
- package/templates/public/assets/icons/expand.svg +31 -0
- package/templates/public/assets/icons/file-blank.svg +17 -0
- package/templates/public/assets/icons/file-image.svg +19 -0
- package/templates/public/assets/icons/file-written.svg +17 -0
- package/templates/public/assets/icons/folder-closed.svg +17 -0
- package/templates/public/assets/icons/folder-open.svg +17 -0
- package/templates/public/assets/icons/hamburger.svg +18 -0
- package/templates/public/assets/icons/home-outline.svg +28 -0
- package/templates/public/assets/icons/home.svg +30 -0
- package/templates/public/assets/icons/hourglass.svg +25 -0
- package/templates/public/assets/icons/information-circle.svg +14 -0
- package/templates/public/assets/icons/information.svg +17 -0
- package/templates/public/assets/icons/lightning.svg +14 -0
- package/templates/public/assets/icons/locked.svg +17 -0
- package/templates/public/assets/icons/not-allowed.svg +14 -0
- package/templates/public/assets/icons/plus.svg +5 -0
- package/templates/public/assets/icons/power-thin.svg +17 -0
- package/templates/public/assets/icons/power.svg +17 -0
- package/templates/public/assets/icons/question-block.svg +14 -0
- package/templates/public/assets/icons/question.svg +17 -0
- package/templates/public/assets/icons/refresh-block.svg +14 -0
- package/templates/public/assets/icons/refresh.svg +17 -0
- package/templates/public/assets/icons/save.svg +14 -0
- package/templates/public/assets/icons/search.svg +25 -0
- package/templates/public/assets/icons/settings.svg +14 -0
- package/templates/public/assets/icons/trash-full.svg +21 -0
- package/templates/public/assets/icons/trash-open.svg +23 -0
- package/templates/public/assets/icons/trash.svg +18 -0
- package/templates/public/assets/icons/unlocked.svg +17 -0
- package/templates/public/assets/icons/waring-triangle-filled.svg +17 -0
- package/templates/public/assets/icons/warning-triangle-filled-2.svg +30 -0
- package/templates/public/assets/icons/warning-triangle-lines.svg +29 -0
- package/templates/public/assets/icons/wrench.svg +17 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Types
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
interface BreadcrumbItem {
|
|
11
|
+
/** Display label */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Navigation href (optional for current/last item) */
|
|
14
|
+
href?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface BreadcrumbsProps {
|
|
18
|
+
/** Breadcrumb items */
|
|
19
|
+
items: BreadcrumbItem[];
|
|
20
|
+
/** Separator character */
|
|
21
|
+
separator?: string;
|
|
22
|
+
/** Additional className */
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Component
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Breadcrumbs component - Navigation hierarchy
|
|
32
|
+
*/
|
|
33
|
+
export function Breadcrumbs({
|
|
34
|
+
items,
|
|
35
|
+
separator = '/',
|
|
36
|
+
className = '',
|
|
37
|
+
}: BreadcrumbsProps) {
|
|
38
|
+
if (items.length === 0) return null;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<nav
|
|
42
|
+
aria-label="Breadcrumb"
|
|
43
|
+
className={`flex items-center gap-2 ${className}`.trim()}
|
|
44
|
+
>
|
|
45
|
+
<ol className="flex items-center gap-2">
|
|
46
|
+
{items.map((item, index) => {
|
|
47
|
+
const isLast = index === items.length - 1;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<li key={index} className="flex items-center gap-2">
|
|
51
|
+
{/* Separator */}
|
|
52
|
+
{index > 0 && (
|
|
53
|
+
<span
|
|
54
|
+
className="font-mondwest text-base text-black/40"
|
|
55
|
+
aria-hidden="true"
|
|
56
|
+
>
|
|
57
|
+
{separator}
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
{/* Item */}
|
|
62
|
+
{item.href && !isLast ? (
|
|
63
|
+
<Link
|
|
64
|
+
href={item.href}
|
|
65
|
+
className="font-mondwest text-base text-black/60 hover:text-black hover:underline transition-colors"
|
|
66
|
+
>
|
|
67
|
+
{item.label}
|
|
68
|
+
</Link>
|
|
69
|
+
) : (
|
|
70
|
+
<span
|
|
71
|
+
className={`
|
|
72
|
+
font-mondwest text-base
|
|
73
|
+
${isLast ? 'text-black font-semibold' : 'text-black/60'}
|
|
74
|
+
`.trim()}
|
|
75
|
+
aria-current={isLast ? 'page' : undefined}
|
|
76
|
+
>
|
|
77
|
+
{item.label}
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
80
|
+
</li>
|
|
81
|
+
);
|
|
82
|
+
})}
|
|
83
|
+
</ol>
|
|
84
|
+
</nav>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default Breadcrumbs;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { Icon } from '@/components/icons';
|
|
6
|
+
import { Spinner } from './Progress';
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
|
|
13
|
+
type ButtonSize = 'sm' | 'md' | 'lg';
|
|
14
|
+
|
|
15
|
+
interface BaseButtonProps {
|
|
16
|
+
/** Visual variant */
|
|
17
|
+
variant?: ButtonVariant;
|
|
18
|
+
/** Size preset */
|
|
19
|
+
size?: ButtonSize;
|
|
20
|
+
/** Expand to fill container width */
|
|
21
|
+
fullWidth?: boolean;
|
|
22
|
+
/** Square button with icon only (no text) */
|
|
23
|
+
iconOnly?: boolean;
|
|
24
|
+
/** Icon name (filename without .svg extension) */
|
|
25
|
+
iconName?: string;
|
|
26
|
+
/** Show loading spinner (only applies to buttons with icons) */
|
|
27
|
+
loading?: boolean;
|
|
28
|
+
/** Button content (optional when iconOnly is true) */
|
|
29
|
+
children?: React.ReactNode;
|
|
30
|
+
/** Additional className */
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ButtonAsButtonProps extends BaseButtonProps, Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof BaseButtonProps> {
|
|
35
|
+
/** URL for navigation - when provided, button can act as a link */
|
|
36
|
+
href?: undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ButtonAsLinkProps extends BaseButtonProps, Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof BaseButtonProps> {
|
|
40
|
+
/** URL for navigation - renders as Next.js Link */
|
|
41
|
+
href: string;
|
|
42
|
+
/** Whether to render as Next.js Link (true) or use window.open (false) */
|
|
43
|
+
asLink?: boolean;
|
|
44
|
+
/** Target for link navigation (e.g., '_blank') */
|
|
45
|
+
target?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type ButtonProps = ButtonAsButtonProps | ButtonAsLinkProps;
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Styles
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Base styles applied to all buttons
|
|
56
|
+
* - Retro lift effect with box-shadow
|
|
57
|
+
* - NO transitions (instant state changes)
|
|
58
|
+
*/
|
|
59
|
+
const baseStyles = `
|
|
60
|
+
inline-flex items-center
|
|
61
|
+
font-joystix uppercase
|
|
62
|
+
whitespace-nowrap
|
|
63
|
+
cursor-pointer select-none
|
|
64
|
+
border border-black
|
|
65
|
+
rounded-sm
|
|
66
|
+
shadow-[0_1px_0_0_var(--color-black)]
|
|
67
|
+
hover:-translate-y-0.5
|
|
68
|
+
hover:shadow-[0_3px_0_0_var(--color-black)]
|
|
69
|
+
active:translate-y-0.5
|
|
70
|
+
active:shadow-none
|
|
71
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
72
|
+
disabled:hover:translate-y-0 disabled:hover:shadow-[0_1px_0_0_var(--color-black)]
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Size presets
|
|
77
|
+
* All buttons use h-8 (2rem) height for consistency
|
|
78
|
+
* Text sizes: sm=10px, md=12px, lg=14px
|
|
79
|
+
*/
|
|
80
|
+
const sizeStyles: Record<ButtonSize, string> = {
|
|
81
|
+
sm: 'h-8 px-3 text-[10px] gap-3',
|
|
82
|
+
md: 'h-8 px-3 text-xs gap-3',
|
|
83
|
+
lg: 'h-8 px-3 text-sm gap-3',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Icon-only size presets (square buttons)
|
|
88
|
+
* All buttons use w-8 h-8 (2rem) for consistency
|
|
89
|
+
*/
|
|
90
|
+
const iconOnlySizeStyles: Record<ButtonSize, string> = {
|
|
91
|
+
sm: 'w-8 h-8 p-0',
|
|
92
|
+
md: 'w-8 h-8 p-0',
|
|
93
|
+
lg: 'w-8 h-8 p-0',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Variant color schemes
|
|
98
|
+
* - primary: cream bg, black text, yellow on hover
|
|
99
|
+
* - secondary: black bg, cream text, inverts on hover
|
|
100
|
+
* - outline: transparent bg, black border, fills on hover
|
|
101
|
+
* - ghost: no border, subtle hover bg
|
|
102
|
+
*/
|
|
103
|
+
const variantStyles: Record<ButtonVariant, string> = {
|
|
104
|
+
primary: `
|
|
105
|
+
bg-sun-yellow text-black
|
|
106
|
+
hover:bg-sun-yellow
|
|
107
|
+
active:bg-sun-yellow
|
|
108
|
+
`,
|
|
109
|
+
secondary: `
|
|
110
|
+
bg-black text-cream
|
|
111
|
+
hover:bg-warm-cloud hover:text-black
|
|
112
|
+
active:bg-sun-yellow active:text-black
|
|
113
|
+
`,
|
|
114
|
+
outline: `
|
|
115
|
+
bg-transparent text-black
|
|
116
|
+
hover:bg-warm-cloud
|
|
117
|
+
active:bg-sun-yellow
|
|
118
|
+
`,
|
|
119
|
+
ghost: `
|
|
120
|
+
bg-transparent text-black
|
|
121
|
+
border-transparent
|
|
122
|
+
shadow-none
|
|
123
|
+
hover:bg-transparent hover:border-black hover:text-black hover:shadow-none hover:translate-y-0
|
|
124
|
+
active:bg-sun-yellow active:text-black active:border-black active:translate-y-0
|
|
125
|
+
`,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// Helper Functions
|
|
130
|
+
// ============================================================================
|
|
131
|
+
|
|
132
|
+
function getButtonClasses(
|
|
133
|
+
variant: ButtonVariant,
|
|
134
|
+
size: ButtonSize,
|
|
135
|
+
iconOnly: boolean,
|
|
136
|
+
fullWidth: boolean,
|
|
137
|
+
className: string
|
|
138
|
+
): string {
|
|
139
|
+
return [
|
|
140
|
+
baseStyles,
|
|
141
|
+
iconOnly ? iconOnlySizeStyles[size] : sizeStyles[size],
|
|
142
|
+
iconOnly ? 'justify-center' : 'justify-start',
|
|
143
|
+
variantStyles[variant],
|
|
144
|
+
fullWidth ? 'w-full' : '',
|
|
145
|
+
className,
|
|
146
|
+
]
|
|
147
|
+
.join(' ')
|
|
148
|
+
.replace(/\s+/g, ' ')
|
|
149
|
+
.trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// Component
|
|
154
|
+
// ============================================================================
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Button component with retro lift effect
|
|
158
|
+
*
|
|
159
|
+
* Supports both button and link behaviors:
|
|
160
|
+
* - Without href: renders as <button>
|
|
161
|
+
* - With href + asLink=true (default): renders as Next.js <Link>
|
|
162
|
+
* - With href + asLink=false: renders as <button> that opens URL via window.open
|
|
163
|
+
*/
|
|
164
|
+
export function Button(props: ButtonProps) {
|
|
165
|
+
const {
|
|
166
|
+
variant = 'primary',
|
|
167
|
+
size = 'md',
|
|
168
|
+
fullWidth = false,
|
|
169
|
+
iconOnly = false,
|
|
170
|
+
iconName,
|
|
171
|
+
loading = false,
|
|
172
|
+
children,
|
|
173
|
+
className = '',
|
|
174
|
+
...rest
|
|
175
|
+
} = props;
|
|
176
|
+
|
|
177
|
+
const classes = getButtonClasses(variant, size, iconOnly, fullWidth, className);
|
|
178
|
+
|
|
179
|
+
// Determine icon size based on button size
|
|
180
|
+
const iconSize = size === 'sm' ? 14 : size === 'lg' ? 18 : 16;
|
|
181
|
+
|
|
182
|
+
// Only show loading spinner for buttons with icons
|
|
183
|
+
const hasIcon = Boolean(iconName || iconOnly);
|
|
184
|
+
const showLoading: boolean = Boolean(loading && hasIcon);
|
|
185
|
+
|
|
186
|
+
// Render content with optional icon or loading spinner
|
|
187
|
+
const content = showLoading ? (
|
|
188
|
+
<>
|
|
189
|
+
<Spinner size={iconSize} />
|
|
190
|
+
{!iconOnly && children}
|
|
191
|
+
</>
|
|
192
|
+
) : iconName ? (
|
|
193
|
+
<>
|
|
194
|
+
<Icon name={iconName} size={iconSize} />
|
|
195
|
+
{!iconOnly && children}
|
|
196
|
+
</>
|
|
197
|
+
) : (
|
|
198
|
+
children
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Check if this is a link variant
|
|
202
|
+
if ('href' in props && props.href) {
|
|
203
|
+
const { href, asLink = true, target, ...linkRest } = rest as ButtonAsLinkProps;
|
|
204
|
+
|
|
205
|
+
// Use Next.js Link for navigation
|
|
206
|
+
if (asLink) {
|
|
207
|
+
return (
|
|
208
|
+
<Link
|
|
209
|
+
href={href}
|
|
210
|
+
target={target}
|
|
211
|
+
className={classes}
|
|
212
|
+
{...(linkRest as Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'target' | 'className'>)}
|
|
213
|
+
>
|
|
214
|
+
{content}
|
|
215
|
+
</Link>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Use window.open via button click
|
|
220
|
+
const linkButtonDisabled: boolean = showLoading || Boolean((linkRest as React.ButtonHTMLAttributes<HTMLButtonElement>).disabled);
|
|
221
|
+
const { disabled: _, ...linkButtonRest } = linkRest as React.ButtonHTMLAttributes<HTMLButtonElement>;
|
|
222
|
+
return (
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
className={classes}
|
|
226
|
+
onClick={() => window.open(href, target || '_self')}
|
|
227
|
+
disabled={linkButtonDisabled}
|
|
228
|
+
{...linkButtonRest}
|
|
229
|
+
>
|
|
230
|
+
{content}
|
|
231
|
+
</button>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Standard button
|
|
236
|
+
const buttonProps = rest as ButtonAsButtonProps;
|
|
237
|
+
|
|
238
|
+
// Disable button when loading
|
|
239
|
+
const disabled: boolean = showLoading || Boolean(buttonProps.disabled);
|
|
240
|
+
const { disabled: _, ...buttonPropsWithoutDisabled } = buttonProps;
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<button className={classes} {...buttonPropsWithoutDisabled} disabled={disabled}>
|
|
244
|
+
{content}
|
|
245
|
+
</button>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export default Button;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Types
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
type CardVariant = 'default' | 'dark' | 'raised';
|
|
10
|
+
|
|
11
|
+
interface CardProps {
|
|
12
|
+
/** Visual variant */
|
|
13
|
+
variant?: CardVariant;
|
|
14
|
+
/** Card content */
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
/** Additional classes */
|
|
17
|
+
className?: string;
|
|
18
|
+
/** Optional padding override */
|
|
19
|
+
noPadding?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Styles
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Base styles for all cards
|
|
28
|
+
*/
|
|
29
|
+
const baseStyles = `
|
|
30
|
+
border border-black
|
|
31
|
+
rounded-md
|
|
32
|
+
overflow-hidden
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Variant styles
|
|
37
|
+
* - default: cream bg, black text
|
|
38
|
+
* - dark: black bg, cream text
|
|
39
|
+
* - raised: cream bg with pixel shadow effect
|
|
40
|
+
*/
|
|
41
|
+
const variantStyles: Record<CardVariant, string> = {
|
|
42
|
+
default: `
|
|
43
|
+
bg-warm-cloud text-black
|
|
44
|
+
`,
|
|
45
|
+
dark: `
|
|
46
|
+
bg-black text-cream
|
|
47
|
+
`,
|
|
48
|
+
raised: `
|
|
49
|
+
bg-warm-cloud text-black
|
|
50
|
+
shadow-[2px_2px_0_0_var(--color-black)]
|
|
51
|
+
`,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Component
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Card container component with consistent styling
|
|
60
|
+
*/
|
|
61
|
+
export function Card({
|
|
62
|
+
variant = 'default',
|
|
63
|
+
children,
|
|
64
|
+
className = '',
|
|
65
|
+
noPadding = false,
|
|
66
|
+
}: CardProps) {
|
|
67
|
+
const classes = [
|
|
68
|
+
baseStyles,
|
|
69
|
+
variantStyles[variant],
|
|
70
|
+
noPadding ? '' : 'p-4',
|
|
71
|
+
className,
|
|
72
|
+
]
|
|
73
|
+
.join(' ')
|
|
74
|
+
.replace(/\s+/g, ' ')
|
|
75
|
+
.trim();
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className={classes}>
|
|
79
|
+
{children}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Card Sub-components
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
interface CardHeaderProps {
|
|
89
|
+
children: React.ReactNode;
|
|
90
|
+
className?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Card header with bottom border
|
|
95
|
+
*/
|
|
96
|
+
export function CardHeader({ children, className = '' }: CardHeaderProps) {
|
|
97
|
+
return (
|
|
98
|
+
<div className={`px-4 py-3 border-b border-black ${className}`}>
|
|
99
|
+
{children}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface CardBodyProps {
|
|
105
|
+
children: React.ReactNode;
|
|
106
|
+
className?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Card body with standard padding
|
|
111
|
+
*/
|
|
112
|
+
export function CardBody({ children, className = '' }: CardBodyProps) {
|
|
113
|
+
return (
|
|
114
|
+
<div className={`p-4 ${className}`}>
|
|
115
|
+
{children}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface CardFooterProps {
|
|
121
|
+
children: React.ReactNode;
|
|
122
|
+
className?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Card footer with top border
|
|
127
|
+
*/
|
|
128
|
+
export function CardFooter({ children, className = '' }: CardFooterProps) {
|
|
129
|
+
return (
|
|
130
|
+
<div className={`px-4 py-3 border-t border-black ${className}`}>
|
|
131
|
+
{children}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default Card;
|
|
137
|
+
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { forwardRef } from 'react';
|
|
4
|
+
import { Icon } from '@/components/icons';
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Types
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
11
|
+
/** Label text */
|
|
12
|
+
label?: string;
|
|
13
|
+
/** Additional classes for container */
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface RadioProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
18
|
+
/** Label text */
|
|
19
|
+
label?: string;
|
|
20
|
+
/** Additional classes for container */
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Checkbox Component
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Retro-styled checkbox
|
|
30
|
+
*/
|
|
31
|
+
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox(
|
|
32
|
+
{ label, className = '', disabled, ...props },
|
|
33
|
+
ref
|
|
34
|
+
) {
|
|
35
|
+
return (
|
|
36
|
+
<label
|
|
37
|
+
className={`
|
|
38
|
+
inline-flex items-center gap-2 cursor-pointer
|
|
39
|
+
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
|
40
|
+
${className}
|
|
41
|
+
`}
|
|
42
|
+
>
|
|
43
|
+
<div className="relative">
|
|
44
|
+
<input
|
|
45
|
+
ref={ref}
|
|
46
|
+
type="checkbox"
|
|
47
|
+
disabled={disabled}
|
|
48
|
+
className="peer sr-only"
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
{/* Custom checkbox visual */}
|
|
52
|
+
<div
|
|
53
|
+
className={`
|
|
54
|
+
w-5 h-5
|
|
55
|
+
bg-warm-cloud
|
|
56
|
+
border border-black
|
|
57
|
+
rounded-xs
|
|
58
|
+
peer-checked:bg-sun-yellow
|
|
59
|
+
peer-focus:ring-2 peer-focus:ring-sun-yellow peer-focus:ring-offset-1
|
|
60
|
+
flex items-center justify-center
|
|
61
|
+
`}
|
|
62
|
+
/>
|
|
63
|
+
{/* Checkmark - visible when checkbox is checked */}
|
|
64
|
+
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 peer-checked:opacity-100 transition-opacity pointer-events-none">
|
|
65
|
+
<Icon
|
|
66
|
+
name="checkmark"
|
|
67
|
+
size={14}
|
|
68
|
+
className="text-black"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
{label && (
|
|
73
|
+
<span className="font-mondwest text-base text-black select-none">
|
|
74
|
+
{label}
|
|
75
|
+
</span>
|
|
76
|
+
)}
|
|
77
|
+
</label>
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// Radio Component
|
|
83
|
+
// ============================================================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Retro-styled radio button
|
|
87
|
+
*/
|
|
88
|
+
export const Radio = forwardRef<HTMLInputElement, RadioProps>(function Radio(
|
|
89
|
+
{ label, className = '', disabled, ...props },
|
|
90
|
+
ref
|
|
91
|
+
) {
|
|
92
|
+
return (
|
|
93
|
+
<label
|
|
94
|
+
className={`
|
|
95
|
+
inline-flex items-center gap-2 cursor-pointer
|
|
96
|
+
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
|
97
|
+
${className}
|
|
98
|
+
`}
|
|
99
|
+
>
|
|
100
|
+
<div className="relative">
|
|
101
|
+
<input
|
|
102
|
+
ref={ref}
|
|
103
|
+
type="radio"
|
|
104
|
+
disabled={disabled}
|
|
105
|
+
className="peer sr-only"
|
|
106
|
+
{...props}
|
|
107
|
+
/>
|
|
108
|
+
{/* Custom radio visual */}
|
|
109
|
+
<div
|
|
110
|
+
className={`
|
|
111
|
+
w-5 h-5
|
|
112
|
+
bg-warm-cloud
|
|
113
|
+
border border-black
|
|
114
|
+
rounded-full
|
|
115
|
+
peer-checked:bg-sun-yellow
|
|
116
|
+
peer-focus:ring-2 peer-focus:ring-sun-yellow peer-focus:ring-offset-1
|
|
117
|
+
flex items-center justify-center
|
|
118
|
+
`}
|
|
119
|
+
>
|
|
120
|
+
{/* Inner dot placeholder */}
|
|
121
|
+
</div>
|
|
122
|
+
{/* Inner dot when checked */}
|
|
123
|
+
<div
|
|
124
|
+
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-2 h-2 bg-black rounded-full opacity-0 peer-checked:opacity-100 pointer-events-none"
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
{label && (
|
|
128
|
+
<span className="font-mondwest text-base text-black select-none">
|
|
129
|
+
{label}
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
</label>
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export default Checkbox;
|
|
137
|
+
|