/** * Enhanced Button Component (Phase 5.2) * * SwiftUI-inspired Button component with press states, variants, * and advanced interaction handling. */ import type { ModifiableComponentWithModifiers, ComponentProps, Signal } from '@tachui/core'; import { ColorAsset } from '@tachui/core'; import type { CloneableComponent, CloneOptions } from '@tachui/core/runtime/types'; import type { Concatenatable, ComponentSegment } from '@tachui/core'; import { ConcatenatedComponent } from '@tachui/core'; import { ComponentWithCSSClasses, type CSSClassesProps } from '@tachui/core'; /** * Button role types following SwiftUI patterns */ export type ButtonRole = 'destructive' | 'cancel' | 'none'; /** * Button variants */ export type ButtonVariant = 'filled' | 'outlined' | 'plain' | 'bordered' | 'borderedProminent'; /** * Button size presets */ export type ButtonSize = 'small' | 'medium' | 'large'; /** * Button state */ export type ButtonState = 'normal' | 'pressed' | 'disabled' | 'focused'; /** * Button component properties with CSS classes support */ export interface ButtonProps extends ComponentProps, CSSClassesProps { title?: string | (() => string) | Signal; systemImage?: string; action?: () => void | Promise; role?: ButtonRole; isEnabled?: boolean | Signal; variant?: ButtonVariant; size?: ButtonSize; tint?: string | Signal | ColorAsset; backgroundColor?: string | Signal | ColorAsset; foregroundColor?: string | Signal | ColorAsset; isPressed?: Signal; isLoading?: boolean | Signal; accessibilityLabel?: string; accessibilityHint?: string; controlSize?: ButtonSize; hapticFeedback?: boolean; } /** * Button theme configuration */ export interface ButtonTheme { colors: { primary: string; secondary: string; destructive: string; background: string; surface: string; onPrimary: string; onSecondary: string; onSurface: string; border: string; disabled: string; }; spacing: { small: number; medium: number; large: number; }; borderRadius: { small: number; medium: number; large: number; }; typography: { small: { size: number; weight: string; }; medium: { size: number; weight: string; }; large: { size: number; weight: string; }; }; } /** * Default button theme */ export declare const defaultButtonTheme: ButtonTheme; /** * Enhanced Button component class with CSS classes support */ export declare class EnhancedButton extends ComponentWithCSSClasses implements CloneableComponent, Concatenatable { props: ButtonProps; readonly type: "component"; readonly id: string; mounted: boolean; cleanup: (() => void)[]; stateSignal: () => ButtonState; private setState; theme: ButtonTheme; /** * CSS properties this component has written to its element. * * What separates "a modifier set this" from "I set this last time", which the * element's own value cannot answer. */ private readonly ownStyleProperties; /** So the ownerless-render notice is said once, not once per prop. */ private warnedOwnerless; constructor(props: ButtonProps, theme?: ButtonTheme); /** * Set up DOM event listeners for button interactions */ private setupDOMEventListeners; /** * Set up reactive style updates based on state changes */ private setupReactiveStyles; /** * Apply computed styles to the button element, respecting modifier-applied styles */ private applyStylesToElement; /** * Convert camelCase to kebab-case for CSS properties */ private camelToKebabCase; /** * Resolve color value from string, signal, or asset */ private resolveColorValue; /** * Check if button is enabled. * * Resolves, the way `isLoading` already did. Handing back the signal itself * made every consumer wrong in the same direction: `if (!isEnabled)` on a * function is never true, so disabled styling never applied and the press * guard never fired, while a reactive effect reading this subscribed to * nothing. */ isEnabled(): boolean; /** * The `isEnabled` prop in the form it was given, signal and all. * * `render` needs the unresolved form: a boolean it can hand straight to the * renderer, or a signal the renderer can subscribe to. Everything else wants * the resolved value and should use {@link isEnabled}. */ private enabledSource; /** * `disabled` as the renderer wants it. * * The renderer subscribes to a prop whose value is a signal and re-applies * it on change, which is the only path that works here — the reactive style * effect runs on DOM-ready, which `renderComponent` never fires. * * Built fresh on every render rather than cached, and the identity matters * as much as the value. A render happens inside an effect, so an enclosing * component re-rendering for any reason at all disposes this scope and takes * the renderer's prop subscription with it. The renderer then diffs the new * props against the old by identity and skips anything unchanged — so a * cached accessor would be recognised, skipped, and never resubscribed, * leaving the attribute frozen at whatever it last read. A new memo each * time is a prop that has changed, which is precisely what has happened. */ /** * The computed styles, as the renderer wants them. * * Owned the same way `disabled` is, and for the same reason: a subscription * made where nothing disposes it is held for the life of the process. With an * owner the renderer tracks the styles and re-applies them; without one they * are a snapshot, which is still more than the DOM-ready effect managed on * that path. * * Applied *before* modifiers, which is the ordering that makes modifier * precedence work by construction rather than by inspecting what is already * on the element. */ /** * Says once, in development, that this Button is rendering where nothing can * dispose a subscription — so its reactive props are snapshots. * * The alternative is a control that silently stops tracking depending on how * its container happened to mount it. The comparison is written inline so a * bundler can drop it. */ private warnOwnerlessRender; private styleProp; private disabledProp; /** * Render the button component */ render(): { type: "element"; tag: string; props: { debugLabel?: string | undefined; className: string | Signal; type: string; disabled: boolean | (() => boolean); style: Record | (() => Record); onClick: (() => void) | undefined; }; children: import("@tachui/core").DOMNode[]; componentMetadata: { id: string; type: string; }; }[]; /** * Check if button is in loading state */ isLoading(): boolean; /** * Check if the button has color-related modifiers applied */ private hasColorModifiers; /** * Check if the button has typography-related modifiers applied */ private hasTypographyModifiers; /** * Check if the button has a shadow modifier applied */ private hasShadowModifier; /** * Get computed button styles based on variant, size, role, and state */ getButtonStyles(): Record; /** * Handle button press with proper state management */ handlePress(): Promise; /** * Trigger haptic feedback (mobile Safari support) */ private triggerHapticFeedback; /** * Helper to darken a color for pressed states */ private darkenColor; /** * Concatenate this button with another concatenatable component */ concat>(other: U): ConcatenatedComponent; /** * Convert this button to a segment for concatenation */ toSegment(): ComponentSegment; /** * Check if this component supports concatenation */ isConcatenatable(): boolean; /** * Determine accessibility role for concatenation */ private determineAccessibilityRole; /** * Merge accessibility roles when combining components */ private mergeAccessibilityRoles; clone(options?: CloneOptions): this; shallowClone(): this; deepClone(): this; private syncStateToClone; } /** * Create enhanced Button component with modifier support */ export declare function Button(title: string | (() => string) | Signal, action?: () => void | Promise, props?: Omit): ModifiableComponentWithModifiers; /** * Props-second form, matching every other primitive — `Image(src, props)`, * `Toggle(isOn, props)`, `Text(content, props)`. Put `action` in the props * object. */ export declare function Button(title: string | (() => string) | Signal, props?: Omit): ModifiableComponentWithModifiers; type ButtonTitle = string | (() => string) | Signal; type ButtonAction = () => void | Promise; /** * The call shapes every `ButtonStyles.*` helper accepts. * * Two overloads rather than one signature admitting both, mirroring `Button` * itself. A single `(title, actionOrProps?, props?)` signature compiles * `Filled('a', { css: 'x' }, { disabled: true })` — an object second *and* a * third argument — which the implementation cannot honour: it branches on * `typeof actionOrProps === 'function'`, takes the object path, and forwards * only the second, dropping the third with no error and no warning (#307). * Splitting the shapes makes that call match no overload, so it fails to * compile instead of silently losing props. * * `TReserved` names the prop the helper sets for you — `variant` for `Filled` * and friends, `role` for `Destructive` and `Cancel` — and `Omit`s it from both * forms, carrying forward what these signatures already declared. * * Note that the `Omit` documents the intent without enforcing it: `ButtonProps` * inherits `[key: string]: any` from `ComponentProps`, so the index signature * still admits the key that `Omit` removed. Passing `variant` to `Filled` * compiles and is then overwritten. Closing that would mean intersecting a * `{ [K in TReserved]?: never }`, which turns a call that compiles today into * an error — a separate change from this one, and not a patch-safe one. */ interface ButtonStyleHelper { (title: ButtonTitle, action?: ButtonAction, props?: Omit): ModifiableComponentWithModifiers; (title: ButtonTitle, props?: Omit): ModifiableComponentWithModifiers; } /** * Button variant shortcuts */ export declare const ButtonStyles: { /** * Filled button (primary) */ Filled: ButtonStyleHelper<"variant">; /** * Outlined button */ Outlined: ButtonStyleHelper<"variant">; /** * Plain button (text only) */ Plain: ButtonStyleHelper<"variant">; /** * Bordered button */ Bordered: ButtonStyleHelper<"variant">; /** * Destructive button */ Destructive: ButtonStyleHelper<"role">; /** * Cancel button */ Cancel: ButtonStyleHelper<"role">; }; export {}; //# sourceMappingURL=Button.d.ts.map