App reorganization

This commit is contained in:
adilallo
2026-04-18 14:12:49 -06:00
parent f866d11ff8
commit e9dab04b34
288 changed files with 2698 additions and 5029 deletions
@@ -3,10 +3,6 @@
import { memo } from "react";
import { AlertView } from "./Alert.view";
import type { AlertProps } from "./Alert.types";
import {
normalizeAlertStatus,
normalizeAlertType,
} from "../../../../lib/propNormalization";
const AlertContainer = memo<AlertProps>(
({
@@ -19,9 +15,8 @@ const AlertContainer = memo<AlertProps>(
onClose,
className = "",
}) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const status = normalizeAlertStatus(statusProp);
const type = normalizeAlertType(typeProp);
const status = statusProp;
const type = typeProp;
// Determine background and border colors based on status and type
const getStatusStyles = () => {
switch (status) {
+4 -14
View File
@@ -1,26 +1,16 @@
export type AlertStatusValue =
| "default"
| "positive"
| "warning"
| "danger"
| "Default"
| "Positive"
| "Warning"
| "Danger";
export type AlertStatusValue = "default" | "positive" | "warning" | "danger";
export type AlertTypeValue = "toast" | "banner" | "Toast" | "Banner";
export type AlertTypeValue = "toast" | "banner";
export interface AlertProps {
title: string;
description?: string;
/**
* Alert status. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Alert status.
*/
status?: AlertStatusValue;
/**
* Alert type. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Alert type.
*/
type?: AlertTypeValue;
/**
@@ -0,0 +1,41 @@
"use client";
import { forwardRef, memo } from "react";
interface ContextMenuProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
children?: React.ReactNode;
}
const ContextMenu = forwardRef<HTMLDivElement, ContextMenuProps>(
({ className = "", children, ...props }, ref) => {
const menuClasses = `
bg-black
border border-[var(--color-border-default-tertiary)]
rounded-[var(--measures-radius-medium)]
shadow-lg
p-[4px]
min-w-[200px]
max-w-[300px]
${className}
`
.trim()
.replace(/\s+/g, " ");
return (
<div
ref={ref}
className={menuClasses}
role="menu"
style={{ backgroundColor: "#000000" }}
{...props}
>
{children}
</div>
);
},
);
ContextMenu.displayName = "ContextMenu";
export default memo(ContextMenu);
@@ -0,0 +1,27 @@
"use client";
import { forwardRef, memo } from "react";
interface ContextMenuDividerProps extends React.HTMLAttributes<HTMLDivElement> {
className?: string;
}
const ContextMenuDivider = forwardRef<HTMLDivElement, ContextMenuDividerProps>(
({ className = "", ...props }, ref) => {
const dividerClasses = `
border-t border-[var(--color-border-default-tertiary)]
my-1
${className}
`
.trim()
.replace(/\s+/g, " ");
return (
<div ref={ref} className={dividerClasses} role="separator" {...props} />
);
},
);
ContextMenuDivider.displayName = "ContextMenuDivider";
export default memo(ContextMenuDivider);
@@ -0,0 +1,36 @@
"use client";
import { forwardRef, memo } from "react";
interface ContextMenuSectionProps extends React.HTMLAttributes<HTMLDivElement> {
title?: string;
children?: React.ReactNode;
className?: string;
}
const ContextMenuSection = forwardRef<HTMLDivElement, ContextMenuSectionProps>(
({ title, children, className = "", ...props }, ref) => {
const sectionClasses = `
${className}
`
.trim()
.replace(/\s+/g, " ");
return (
<div ref={ref} className={sectionClasses} role="group" {...props}>
{title && (
<div className="px-3 py-2">
<div className="text-[var(--color-content-default-primary)] text-sm font-medium">
{title}
</div>
</div>
)}
{children}
</div>
);
},
);
ContextMenuSection.displayName = "ContextMenuSection";
export default memo(ContextMenuSection);
@@ -0,0 +1,101 @@
"use client";
import { forwardRef, memo, useCallback } from "react";
import { ContextMenuItemView } from "./ContextMenuItem.view";
import type { ContextMenuItemProps } from "./ContextMenuItem.types";
const ContextMenuItemContainer = forwardRef<
HTMLDivElement,
ContextMenuItemProps
>(
(
{
children,
selected = false,
hasSubmenu = false,
disabled = false,
className = "",
onClick,
size: sizeProp = "medium",
...props
},
ref,
) => {
const size = sizeProp;
const getTextSize = (): string => {
switch (size) {
case "small":
return "text-[10px] leading-[14px]";
case "medium":
return "text-[14px] leading-[20px]";
case "large":
return "text-[16px] leading-[24px]";
default:
return "text-[14px] leading-[20px]";
}
};
const itemClasses = `
flex items-center justify-between
px-[8px] py-[4px]
text-[var(--color-content-default-brand-primary)]
${getTextSize()}
cursor-pointer
transition-colors duration-150
${
selected
? "bg-[var(--color-surface-default-secondary)] rounded-[var(--measures-radius-small)]"
: ""
}
${
disabled
? "opacity-50 cursor-not-allowed"
: "hover:!bg-[var(--color-surface-default-secondary)] hover:!rounded-[var(--measures-radius-small)]"
}
${className}
`
.trim()
.replace(/\s+/g, " ");
const handleClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (!disabled && onClick) {
onClick(e);
}
},
[disabled, onClick],
);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (!disabled && onClick) {
onClick(e);
}
}
},
[disabled, onClick],
);
return (
<ContextMenuItemView
ref={ref}
selected={selected}
hasSubmenu={hasSubmenu}
disabled={disabled}
className={className}
itemClasses={itemClasses}
handleClick={handleClick}
handleKeyDown={handleKeyDown}
{...props}
>
{children}
</ContextMenuItemView>
);
},
);
ContextMenuItemContainer.displayName = "ContextMenuItem";
export default memo(ContextMenuItemContainer);
@@ -0,0 +1,27 @@
export type ContextMenuItemSizeValue = "small" | "medium" | "large";
export interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
selected?: boolean;
hasSubmenu?: boolean;
disabled?: boolean;
className?: string;
onClick?: (
_e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
) => void;
/**
* Context menu item size.
*/
size?: ContextMenuItemSizeValue;
}
export interface ContextMenuItemViewProps {
children?: React.ReactNode;
selected: boolean;
hasSubmenu: boolean;
disabled: boolean;
className: string;
itemClasses: string;
handleClick: (_e: React.MouseEvent<HTMLDivElement>) => void;
handleKeyDown: (_e: React.KeyboardEvent<HTMLDivElement>) => void;
}
@@ -0,0 +1,71 @@
import { forwardRef } from "react";
import type { ContextMenuItemViewProps } from "./ContextMenuItem.types";
export const ContextMenuItemView = forwardRef<
HTMLDivElement,
ContextMenuItemViewProps
>(
(
{
children,
selected,
hasSubmenu,
disabled,
itemClasses,
handleClick,
handleKeyDown,
...props
},
ref,
) => {
return (
<div
ref={ref}
className={itemClasses}
role="menuitem"
tabIndex={disabled ? -1 : 0}
aria-current={selected ? "true" : undefined}
aria-disabled={disabled}
onClick={handleClick}
onKeyDown={handleKeyDown}
{...props}
>
<div className="flex items-center gap-[8px]">
{selected && (
<svg
className="w-4 h-4 text-[var(--color-content-default-brand-primary)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
)}
<span>{children}</span>
</div>
{hasSubmenu && (
<svg
className="w-4 h-4 text-[var(--color-content-default-brand-primary)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
)}
</div>
);
},
);
ContextMenuItemView.displayName = "ContextMenuItemView";
@@ -0,0 +1,2 @@
export { default } from "./ContextMenuItem.container";
export type { ContextMenuItemProps } from "./ContextMenuItem.types";
+1 -1
View File
@@ -9,7 +9,7 @@ import TextInput from "../../controls/TextInput";
import ContentLockup from "../../type/ContentLockup";
import { requestMagicLink } from "../../../../lib/create/api";
import { safeInternalPath } from "../../../../lib/safeInternalPath";
import { setTransferPendingFlag } from "../../../create/utils/anonymousDraftStorage";
import { setTransferPendingFlag } from "../../../(app)/create/utils/anonymousDraftStorage";
/** Mail icon for login modal (inline SVG; same pattern as InfoMessageBox ExclamationIconInline). */
function MailIconInline() {
@@ -3,7 +3,6 @@
import { memo, useState } from "react";
import { TooltipView } from "./Tooltip.view";
import type { TooltipProps } from "./Tooltip.types";
import { normalizeTooltipPosition } from "../../../../lib/propNormalization";
const TooltipContainer = memo<TooltipProps>(
({
@@ -13,8 +12,7 @@ const TooltipContainer = memo<TooltipProps>(
className = "",
disabled = false,
}) => {
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
const position = normalizeTooltipPosition(positionProp);
const position = positionProp;
const [isVisible, setIsVisible] = useState(false);
if (disabled) {
@@ -1,11 +1,10 @@
export type TooltipPositionValue = "top" | "bottom" | "Top" | "Bottom";
export type TooltipPositionValue = "top" | "bottom";
export interface TooltipProps {
children: React.ReactNode;
text: string;
/**
* Tooltip position. Accepts both lowercase and PascalCase (case-insensitive).
* Figma uses PascalCase, codebase uses lowercase - both are supported.
* Tooltip position.
*/
position?: TooltipPositionValue;
className?: string;