@fanvue/ui 3.18.0 → 3.19.1
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/cjs/components/Autocomplete/Autocomplete.cjs +1 -1
- package/dist/cjs/components/Autocomplete/Autocomplete.cjs.map +1 -1
- package/dist/cjs/components/Badge/Badge.cjs +13 -11
- package/dist/cjs/components/Badge/Badge.cjs.map +1 -1
- package/dist/cjs/components/Banner/Banner.cjs +1 -1
- package/dist/cjs/components/Banner/Banner.cjs.map +1 -1
- package/dist/cjs/components/Chip/Chip.cjs +21 -1
- package/dist/cjs/components/Chip/Chip.cjs.map +1 -1
- package/dist/cjs/components/Count/Count.cjs +36 -7
- package/dist/cjs/components/Count/Count.cjs.map +1 -1
- package/dist/cjs/components/CyclingText/CyclingText.cjs +4 -1
- package/dist/cjs/components/CyclingText/CyclingText.cjs.map +1 -1
- package/dist/cjs/components/PhoneField/PhoneField.cjs +1 -1
- package/dist/cjs/components/PhoneField/PhoneField.cjs.map +1 -1
- package/dist/cjs/components/TextArea/TextArea.cjs +1 -1
- package/dist/cjs/components/TextArea/TextArea.cjs.map +1 -1
- package/dist/cjs/components/TextField/TextField.cjs +1 -1
- package/dist/cjs/components/TextField/TextField.cjs.map +1 -1
- package/dist/components/Autocomplete/Autocomplete.mjs +1 -1
- package/dist/components/Autocomplete/Autocomplete.mjs.map +1 -1
- package/dist/components/Badge/Badge.mjs +13 -11
- package/dist/components/Badge/Badge.mjs.map +1 -1
- package/dist/components/Banner/Banner.mjs +1 -1
- package/dist/components/Banner/Banner.mjs.map +1 -1
- package/dist/components/Chip/Chip.mjs +21 -1
- package/dist/components/Chip/Chip.mjs.map +1 -1
- package/dist/components/Count/Count.mjs +36 -7
- package/dist/components/Count/Count.mjs.map +1 -1
- package/dist/components/CyclingText/CyclingText.mjs +4 -1
- package/dist/components/CyclingText/CyclingText.mjs.map +1 -1
- package/dist/components/PhoneField/PhoneField.mjs +1 -1
- package/dist/components/PhoneField/PhoneField.mjs.map +1 -1
- package/dist/components/TextArea/TextArea.mjs +1 -1
- package/dist/components/TextArea/TextArea.mjs.map +1 -1
- package/dist/components/TextField/TextField.mjs +1 -1
- package/dist/components/TextField/TextField.mjs.map +1 -1
- package/dist/index.d.ts +27 -7
- package/dist/styles/base.css +7 -0
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Banner.mjs","sources":["../../../src/components/Banner/Banner.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Badge, type BadgeVariant } from \"../Badge/Badge\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { CrossIcon } from \"../Icons/CrossIcon\";\n\n/**\n * Matches the Fanvue Library Banner Figma component property `Type`\n * (`Default`, `Subtle`, `whatsNew`, `appStore1`, `appStore2`, `appStore3`).\n */\nexport type BannerVariant =\n | \"Default\"\n | \"Subtle\"\n | \"whatsNew\"\n | \"appStore1\"\n | \"appStore2\"\n | \"appStore3\";\n\n/** Layout (`Vertical` / `Horizontal` / `HorizontalSmall` in Figma). Ignored for `Subtle` and app store types. */\nexport type BannerLayout = \"vertical\" | \"horizontal\" | \"compact\";\n\ntype AppStoreVariant = \"appStore1\" | \"appStore2\" | \"appStore3\";\n\nconst APP_STORE_VARIANTS: readonly AppStoreVariant[] = [\"appStore1\", \"appStore2\", \"appStore3\"];\n\nfunction isAppStoreVariant(variant: BannerVariant): variant is AppStoreVariant {\n return (APP_STORE_VARIANTS as readonly string[]).includes(variant);\n}\n\nconst GUIDE_BADGE_VARIANT: Record<AppStoreVariant, BadgeVariant> = {\n appStore1: \"dark\",\n appStore2: \"default\",\n appStore3: \"default\",\n};\n\nconst BANNER_SHADOW = \"shadow-sm\";\n\nconst guideGradient: Record<AppStoreVariant, React.CSSProperties> = {\n appStore1: {\n backgroundImage:\n \"linear-gradient(125.54deg, var(--color-brand-primary-muted) 0%, var(--color-neutral-alphas-50) 100%)\",\n },\n appStore2: {\n backgroundImage:\n \"linear-gradient(125.54deg, var(--color-brand-secondary-muted) 0%, var(--color-brand-secondary-muted) 100%)\",\n },\n appStore3: {\n backgroundImage:\n \"linear-gradient(125.54deg, var(--color-brand-secondary-muted) 0%, color-mix(in srgb, var(--color-info-surface) 80%, transparent) 100%)\",\n },\n};\n\nexport interface BannerProps extends Omit<React.HTMLAttributes<HTMLElement>, \"title\"> {\n /** Figma `Type` — selects structure, surfaces, and app store gradient. */\n variant: BannerVariant;\n /**\n * Figma `Orientation`. Ignored when `variant` is `Subtle` (horizontal) or an app store type (vertical).\n * @default `\"vertical\"` for `Default`, `\"horizontal\"` for `whatsNew`\n */\n layout?: BannerLayout;\n /** Leading visual (image, illustration, or composite). */\n media?: React.ReactNode;\n /** Small uppercase label (e.g. HOW TO). */\n eyebrow?: React.ReactNode;\n /** Top badge row (e.g. NEW pill) — mainly for `Subtle`. */\n leadBadge?: React.ReactNode;\n /** Main heading. */\n title?: React.ReactNode;\n /** Primary body copy. */\n description?: React.ReactNode;\n /** Extra line under description (`Subtle`). */\n secondaryLine?: React.ReactNode;\n /** Left-stacked pill action (`Subtle`). */\n stackedAction?: React.ReactNode;\n /** Row under stacked action (`Subtle`). */\n statusRow?: React.ReactNode;\n /** Primary button (e.g. Learn more). */\n primaryAction?: React.ReactNode;\n /** Text-style CTA (`whatsNew` / app store). */\n textAction?: React.ReactNode;\n /** When set, shows a dismiss control (`Default` only). */\n onDismiss?: () => void;\n /** Accessible label for dismiss. @default \"Dismiss banner\" */\n dismissLabel?: string;\n}\n\nfunction BannerDismiss({\n onDismiss,\n dismissLabel,\n}: {\n onDismiss: () => void;\n dismissLabel: string;\n}) {\n return (\n <IconButton\n variant=\"contrast\"\n size=\"24\"\n icon={<CrossIcon />}\n onClick={onDismiss}\n aria-label={dismissLabel}\n className=\"hover:bg-white/10 active:bg-white/15\"\n />\n );\n}\n\nfunction resolveLayout(variant: BannerVariant, layoutProp: BannerLayout | undefined): BannerLayout {\n if (variant === \"Subtle\") {\n return \"horizontal\";\n }\n if (isAppStoreVariant(variant)) {\n return \"vertical\";\n }\n if (variant === \"whatsNew\") {\n return layoutProp ?? \"horizontal\";\n }\n return layoutProp ?? \"vertical\";\n}\n\nfunction bannerRootClass(variant: BannerVariant, layout: BannerLayout, className?: string): string {\n return cn(\n \"flex rounded-md\",\n BANNER_SHADOW,\n variant === \"Default\" && \"gap-3 bg-surface-primary-inverted p-4 text-content-primary-inverted\",\n variant === \"Subtle\" &&\n \"w-full max-w-[600px] items-start gap-3 border border-border-primary bg-surface-secondary p-4 text-content-primary\",\n variant === \"whatsNew\" &&\n layout === \"horizontal\" &&\n \"w-full max-w-[446px] items-center gap-4 bg-surface-purple-muted p-4 text-content-primary\",\n variant === \"whatsNew\" &&\n layout === \"vertical\" &&\n \"w-full max-w-[220px] flex-col items-stretch gap-4 border border-border-primary bg-surface-secondary p-4 text-content-primary\",\n variant === \"whatsNew\" &&\n layout === \"compact\" &&\n \"w-full max-w-[446px] items-start gap-4 bg-surface-purple-muted p-4 text-content-primary\",\n isAppStoreVariant(variant) && \"w-full max-w-[280px] flex-col gap-4 p-6\",\n layout === \"vertical\" && variant === \"Default\" && \"max-w-[360px]\",\n layout === \"horizontal\" && variant === \"Default\" && \"w-full max-w-[600px] items-start\",\n className,\n );\n}\n\ntype BannerSectionProps = React.ComponentPropsWithoutRef<\"section\"> & {\n labelledBy?: string;\n};\n\nconst BannerSection = React.forwardRef<HTMLElement, BannerSectionProps>(\n ({ className, labelledBy, children, ...rest }, ref) => (\n <section\n ref={ref}\n aria-labelledby={labelledBy}\n data-testid=\"banner\"\n className={className}\n {...rest}\n >\n {children}\n </section>\n ),\n);\nBannerSection.displayName = \"BannerSection\";\n\ntype GuideBodyProps = Pick<BannerProps, \"eyebrow\" | \"title\" | \"description\" | \"textAction\"> & {\n appStoreVariant: AppStoreVariant;\n labelledBy?: string;\n};\n\nfunction BannerGuideBody({\n appStoreVariant,\n eyebrow,\n title,\n description,\n textAction,\n labelledBy,\n}: GuideBodyProps) {\n return (\n <>\n {eyebrow !== undefined && eyebrow !== null && eyebrow !== false && (\n <Badge\n variant={GUIDE_BADGE_VARIANT[appStoreVariant]}\n leftDot={false}\n className=\"typography-badge-badgecaps self-start\"\n >\n {eyebrow}\n </Badge>\n )}\n {title !== undefined && title !== null && title !== false && (\n <p id={labelledBy} className=\"typography-body-default-16px-semibold text-content-primary\">\n {title}\n </p>\n )}\n {description !== undefined && description !== null && description !== false && (\n <p className=\"typography-body-small-14px-regular text-content-secondary\">{description}</p>\n )}\n {textAction}\n </>\n );\n}\n\ntype FeatureBodyProps = Pick<BannerProps, \"title\" | \"description\" | \"textAction\" | \"media\"> & {\n layout: BannerLayout;\n labelledBy?: string;\n};\n\nfunction BannerFeatureBody({\n layout,\n media,\n title,\n description,\n textAction,\n labelledBy,\n}: FeatureBodyProps) {\n const titleClass =\n layout === \"vertical\"\n ? \"typography-body-default-16px-semibold text-content-primary\"\n : \"typography-body-default-16px-semibold text-[18px] leading-6 text-content-primary\";\n const mediaWrap =\n layout === \"compact\"\n ? \"size-20 shrink-0 overflow-hidden rounded-sm\"\n : \"size-[132px] shrink-0 overflow-hidden rounded-sm\";\n\n return (\n <>\n {media !== undefined && media !== null && (\n <div className={cn(mediaWrap, layout === \"vertical\" && \"w-full [&>*]:mx-auto\")}>\n {media}\n </div>\n )}\n <div\n className={cn(\n \"flex min-w-0 flex-col gap-2\",\n layout === \"horizontal\" || layout === \"compact\" ? \"flex-1 justify-end\" : \"w-full\",\n )}\n >\n {title !== undefined && title !== null && title !== false && (\n <div id={labelledBy} className={titleClass}>\n {title}\n </div>\n )}\n {description !== undefined && description !== null && description !== false && (\n <p className=\"typography-body-small-14px-regular text-content-secondary\">{description}</p>\n )}\n {textAction}\n </div>\n </>\n );\n}\n\ntype SubtleBodyProps = Pick<\n BannerProps,\n | \"media\"\n | \"leadBadge\"\n | \"title\"\n | \"description\"\n | \"secondaryLine\"\n | \"stackedAction\"\n | \"statusRow\"\n | \"primaryAction\"\n> & { labelledBy?: string };\n\nfunction BannerSubtleBody({\n media,\n leadBadge,\n title,\n description,\n secondaryLine,\n stackedAction,\n statusRow,\n primaryAction,\n labelledBy,\n}: SubtleBodyProps) {\n const mediaSizeDefault = \"size-12 shrink-0 overflow-hidden rounded-xl\";\n return (\n <>\n {media !== undefined && media !== null && <div className={mediaSizeDefault}>{media}</div>}\n <div className=\"flex min-w-0 flex-1 items-end gap-3\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-4\">\n {leadBadge !== undefined && leadBadge !== null && (\n <div className=\"flex flex-wrap items-center gap-2\">{leadBadge}</div>\n )}\n <div className=\"flex flex-col gap-1\">\n {title !== undefined && title !== null && title !== false && (\n <div id={labelledBy} className=\"typography-header-heading-xs text-content-primary\">\n {title}\n </div>\n )}\n <div className=\"flex flex-col gap-2\">\n {description !== undefined && description !== null && description !== false && (\n <p className=\"typography-body-small-14px-regular text-content-primary\">\n {description}\n </p>\n )}\n {secondaryLine !== undefined && secondaryLine !== null && secondaryLine !== false && (\n <p className=\"typography-description-12px-regular text-content-primary\">\n {secondaryLine}\n </p>\n )}\n </div>\n </div>\n {stackedAction !== undefined && stackedAction !== null && (\n <div className=\"flex flex-col items-end self-start\">{stackedAction}</div>\n )}\n {statusRow !== undefined && statusRow !== null && (\n <div className=\"flex flex-wrap items-center gap-2\">{statusRow}</div>\n )}\n </div>\n {primaryAction !== undefined && primaryAction !== null && (\n <div className=\"shrink-0 self-center\">{primaryAction}</div>\n )}\n </div>\n </>\n );\n}\n\ntype InverseBodyProps = Pick<\n BannerProps,\n \"media\" | \"eyebrow\" | \"title\" | \"description\" | \"primaryAction\"\n> & {\n layout: BannerLayout;\n labelledBy?: string;\n dismissSlot: React.ReactNode;\n};\n\nfunction BannerInverseBody({\n layout,\n media,\n eyebrow,\n title,\n description,\n primaryAction,\n labelledBy,\n dismissSlot,\n}: InverseBodyProps) {\n const mediaSizeDefault = \"size-12 shrink-0 overflow-hidden rounded-xl\";\n const titleClassInverse = \"typography-header-heading-xs text-content-primary-inverted\";\n const textColumn = (\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n {eyebrow !== undefined && eyebrow !== null && eyebrow !== false && (\n <p className=\"typography-description-12px-semibold text-content-primary-inverted\">\n {eyebrow}\n </p>\n )}\n {title !== undefined && title !== null && title !== false && (\n <div id={labelledBy} className={titleClassInverse}>\n {title}\n </div>\n )}\n {description !== undefined && description !== null && description !== false && (\n <p className=\"typography-body-small-14px-regular text-content-primary-inverted\">\n {description}\n </p>\n )}\n </div>\n );\n\n return (\n <>\n {media !== undefined && media !== null && <div className={mediaSizeDefault}>{media}</div>}\n {layout === \"horizontal\" ? (\n <div className=\"flex min-w-0 flex-1 items-end gap-3\">\n {textColumn}\n {primaryAction !== undefined && primaryAction !== null && (\n <div className=\"shrink-0\">{primaryAction}</div>\n )}\n </div>\n ) : (\n <div className=\"flex min-w-0 flex-1 flex-col gap-3\">\n {textColumn}\n {primaryAction !== undefined && primaryAction !== null && <div>{primaryAction}</div>}\n </div>\n )}\n {dismissSlot}\n </>\n );\n}\n\nexport const Banner = React.forwardRef<HTMLElement, BannerProps>(\n (\n {\n className,\n variant,\n layout: layoutProp,\n media,\n eyebrow,\n leadBadge,\n title,\n description,\n secondaryLine,\n stackedAction,\n statusRow,\n primaryAction,\n textAction,\n onDismiss,\n dismissLabel = \"Dismiss banner\",\n ...props\n },\n ref,\n ) => {\n const layout = resolveLayout(variant, layoutProp);\n const showDismiss = onDismiss !== undefined && variant === \"Default\";\n const titleId = React.useId();\n const regionLabelledBy =\n title !== undefined && title !== null && title !== false ? titleId : undefined;\n\n const rootClass = bannerRootClass(variant, layout, className);\n\n if (isAppStoreVariant(variant)) {\n return (\n <BannerSection\n ref={ref}\n labelledBy={regionLabelledBy}\n style={guideGradient[variant]}\n className={rootClass}\n {...props}\n >\n <BannerGuideBody\n appStoreVariant={variant}\n eyebrow={eyebrow}\n title={title}\n description={description}\n textAction={textAction}\n labelledBy={regionLabelledBy}\n />\n </BannerSection>\n );\n }\n\n if (variant === \"whatsNew\") {\n return (\n <BannerSection ref={ref} labelledBy={regionLabelledBy} className={rootClass} {...props}>\n <BannerFeatureBody\n layout={layout}\n media={media}\n title={title}\n description={description}\n textAction={textAction}\n labelledBy={regionLabelledBy}\n />\n </BannerSection>\n );\n }\n\n if (variant === \"Subtle\") {\n return (\n <BannerSection ref={ref} labelledBy={regionLabelledBy} className={rootClass} {...props}>\n <BannerSubtleBody\n media={media}\n leadBadge={leadBadge}\n title={title}\n description={description}\n secondaryLine={secondaryLine}\n stackedAction={stackedAction}\n statusRow={statusRow}\n primaryAction={primaryAction}\n labelledBy={regionLabelledBy}\n />\n </BannerSection>\n );\n }\n\n const dismissSlot =\n showDismiss && onDismiss !== undefined ? (\n <BannerDismiss onDismiss={onDismiss} dismissLabel={dismissLabel} />\n ) : null;\n\n return (\n <BannerSection ref={ref} labelledBy={regionLabelledBy} className={rootClass} {...props}>\n <div className=\"flex w-full items-start gap-3\">\n <BannerInverseBody\n layout={layout}\n media={media}\n eyebrow={eyebrow}\n title={title}\n description={description}\n primaryAction={primaryAction}\n labelledBy={regionLabelledBy}\n dismissSlot={dismissSlot}\n />\n </div>\n </BannerSection>\n );\n },\n);\n\nBanner.displayName = \"Banner\";\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Banner.mjs","sources":["../../../src/components/Banner/Banner.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Badge, type BadgeVariant } from \"../Badge/Badge\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { CrossIcon } from \"../Icons/CrossIcon\";\n\n/**\n * Matches the Fanvue Library Banner Figma component property `Type`\n * (`Default`, `Subtle`, `whatsNew`, `appStore1`, `appStore2`, `appStore3`).\n */\nexport type BannerVariant =\n | \"Default\"\n | \"Subtle\"\n | \"whatsNew\"\n | \"appStore1\"\n | \"appStore2\"\n | \"appStore3\";\n\n/** Layout (`Vertical` / `Horizontal` / `HorizontalSmall` in Figma). Ignored for `Subtle` and app store types. */\nexport type BannerLayout = \"vertical\" | \"horizontal\" | \"compact\";\n\ntype AppStoreVariant = \"appStore1\" | \"appStore2\" | \"appStore3\";\n\nconst APP_STORE_VARIANTS: readonly AppStoreVariant[] = [\"appStore1\", \"appStore2\", \"appStore3\"];\n\nfunction isAppStoreVariant(variant: BannerVariant): variant is AppStoreVariant {\n return (APP_STORE_VARIANTS as readonly string[]).includes(variant);\n}\n\nconst GUIDE_BADGE_VARIANT: Record<AppStoreVariant, BadgeVariant> = {\n appStore1: \"dark\",\n appStore2: \"default\",\n appStore3: \"default\",\n};\n\nconst BANNER_SHADOW = \"shadow-sm\";\n\nconst guideGradient: Record<AppStoreVariant, React.CSSProperties> = {\n appStore1: {\n backgroundImage:\n \"linear-gradient(125.54deg, var(--color-brand-primary-muted) 0%, var(--color-neutral-alphas-50) 100%)\",\n },\n appStore2: {\n backgroundImage:\n \"linear-gradient(125.54deg, var(--color-brand-secondary-muted) 0%, var(--color-brand-secondary-muted) 100%)\",\n },\n appStore3: {\n backgroundImage:\n \"linear-gradient(125.54deg, var(--color-brand-secondary-muted) 0%, color-mix(in srgb, var(--color-info-surface) 80%, transparent) 100%)\",\n },\n};\n\nexport interface BannerProps extends Omit<React.HTMLAttributes<HTMLElement>, \"title\"> {\n /** Figma `Type` — selects structure, surfaces, and app store gradient. */\n variant: BannerVariant;\n /**\n * Figma `Orientation`. Ignored when `variant` is `Subtle` (horizontal) or an app store type (vertical).\n * @default `\"vertical\"` for `Default`, `\"horizontal\"` for `whatsNew`\n */\n layout?: BannerLayout;\n /** Leading visual (image, illustration, or composite). */\n media?: React.ReactNode;\n /** Small uppercase label (e.g. HOW TO). */\n eyebrow?: React.ReactNode;\n /** Top badge row (e.g. NEW pill) — mainly for `Subtle`. */\n leadBadge?: React.ReactNode;\n /** Main heading. */\n title?: React.ReactNode;\n /** Primary body copy. */\n description?: React.ReactNode;\n /** Extra line under description (`Subtle`). */\n secondaryLine?: React.ReactNode;\n /** Left-stacked pill action (`Subtle`). */\n stackedAction?: React.ReactNode;\n /** Row under stacked action (`Subtle`). */\n statusRow?: React.ReactNode;\n /** Primary button (e.g. Learn more). */\n primaryAction?: React.ReactNode;\n /** Text-style CTA (`whatsNew` / app store). */\n textAction?: React.ReactNode;\n /** When set, shows a dismiss control (`Default` only). */\n onDismiss?: () => void;\n /** Accessible label for dismiss. @default \"Dismiss banner\" */\n dismissLabel?: string;\n}\n\nfunction BannerDismiss({\n onDismiss,\n dismissLabel,\n}: {\n onDismiss: () => void;\n dismissLabel: string;\n}) {\n return (\n <IconButton\n variant=\"contrast\"\n size=\"24\"\n icon={<CrossIcon />}\n onClick={onDismiss}\n aria-label={dismissLabel}\n className=\"hover:bg-white/10 active:bg-white/15\"\n />\n );\n}\n\nfunction resolveLayout(variant: BannerVariant, layoutProp: BannerLayout | undefined): BannerLayout {\n if (variant === \"Subtle\") {\n return \"horizontal\";\n }\n if (isAppStoreVariant(variant)) {\n return \"vertical\";\n }\n if (variant === \"whatsNew\") {\n return layoutProp ?? \"horizontal\";\n }\n return layoutProp ?? \"vertical\";\n}\n\nfunction bannerRootClass(variant: BannerVariant, layout: BannerLayout, className?: string): string {\n return cn(\n \"flex rounded-md\",\n BANNER_SHADOW,\n variant === \"Default\" && \"gap-3 bg-surface-primary-inverted p-4 text-content-primary-inverted\",\n variant === \"Subtle\" &&\n \"w-full max-w-[600px] items-start gap-3 border border-border-primary bg-surface-secondary p-4 text-content-primary\",\n variant === \"whatsNew\" &&\n layout === \"horizontal\" &&\n \"w-full max-w-[446px] items-center gap-4 bg-surface-purple-muted p-4 text-content-primary\",\n variant === \"whatsNew\" &&\n layout === \"vertical\" &&\n \"w-full max-w-[220px] flex-col items-stretch gap-4 border border-border-primary bg-surface-secondary p-4 text-content-primary\",\n variant === \"whatsNew\" &&\n layout === \"compact\" &&\n \"w-full max-w-[446px] items-start gap-4 bg-surface-purple-muted p-4 text-content-primary\",\n isAppStoreVariant(variant) && \"w-full max-w-[280px] flex-col gap-4 p-6\",\n layout === \"vertical\" && variant === \"Default\" && \"max-w-[360px]\",\n layout === \"horizontal\" && variant === \"Default\" && \"w-full max-w-[600px] items-start\",\n className,\n );\n}\n\ntype BannerSectionProps = React.ComponentPropsWithoutRef<\"section\"> & {\n labelledBy?: string;\n};\n\nconst BannerSection = React.forwardRef<HTMLElement, BannerSectionProps>(\n ({ className, labelledBy, children, ...rest }, ref) => (\n <section\n ref={ref}\n aria-labelledby={labelledBy}\n data-testid=\"banner\"\n className={className}\n {...rest}\n >\n {children}\n </section>\n ),\n);\nBannerSection.displayName = \"BannerSection\";\n\ntype GuideBodyProps = Pick<BannerProps, \"eyebrow\" | \"title\" | \"description\" | \"textAction\"> & {\n appStoreVariant: AppStoreVariant;\n labelledBy?: string;\n};\n\nfunction BannerGuideBody({\n appStoreVariant,\n eyebrow,\n title,\n description,\n textAction,\n labelledBy,\n}: GuideBodyProps) {\n return (\n <>\n {eyebrow !== undefined && eyebrow !== null && eyebrow !== false && (\n <Badge\n variant={GUIDE_BADGE_VARIANT[appStoreVariant]}\n leftDot={false}\n className=\"self-start\"\n >\n {eyebrow}\n </Badge>\n )}\n {title !== undefined && title !== null && title !== false && (\n <p id={labelledBy} className=\"typography-body-default-16px-semibold text-content-primary\">\n {title}\n </p>\n )}\n {description !== undefined && description !== null && description !== false && (\n <p className=\"typography-body-small-14px-regular text-content-secondary\">{description}</p>\n )}\n {textAction}\n </>\n );\n}\n\ntype FeatureBodyProps = Pick<BannerProps, \"title\" | \"description\" | \"textAction\" | \"media\"> & {\n layout: BannerLayout;\n labelledBy?: string;\n};\n\nfunction BannerFeatureBody({\n layout,\n media,\n title,\n description,\n textAction,\n labelledBy,\n}: FeatureBodyProps) {\n const titleClass =\n layout === \"vertical\"\n ? \"typography-body-default-16px-semibold text-content-primary\"\n : \"typography-body-default-16px-semibold text-[18px] leading-6 text-content-primary\";\n const mediaWrap =\n layout === \"compact\"\n ? \"size-20 shrink-0 overflow-hidden rounded-sm\"\n : \"size-[132px] shrink-0 overflow-hidden rounded-sm\";\n\n return (\n <>\n {media !== undefined && media !== null && (\n <div className={cn(mediaWrap, layout === \"vertical\" && \"w-full [&>*]:mx-auto\")}>\n {media}\n </div>\n )}\n <div\n className={cn(\n \"flex min-w-0 flex-col gap-2\",\n layout === \"horizontal\" || layout === \"compact\" ? \"flex-1 justify-end\" : \"w-full\",\n )}\n >\n {title !== undefined && title !== null && title !== false && (\n <div id={labelledBy} className={titleClass}>\n {title}\n </div>\n )}\n {description !== undefined && description !== null && description !== false && (\n <p className=\"typography-body-small-14px-regular text-content-secondary\">{description}</p>\n )}\n {textAction}\n </div>\n </>\n );\n}\n\ntype SubtleBodyProps = Pick<\n BannerProps,\n | \"media\"\n | \"leadBadge\"\n | \"title\"\n | \"description\"\n | \"secondaryLine\"\n | \"stackedAction\"\n | \"statusRow\"\n | \"primaryAction\"\n> & { labelledBy?: string };\n\nfunction BannerSubtleBody({\n media,\n leadBadge,\n title,\n description,\n secondaryLine,\n stackedAction,\n statusRow,\n primaryAction,\n labelledBy,\n}: SubtleBodyProps) {\n const mediaSizeDefault = \"size-12 shrink-0 overflow-hidden rounded-xl\";\n return (\n <>\n {media !== undefined && media !== null && <div className={mediaSizeDefault}>{media}</div>}\n <div className=\"flex min-w-0 flex-1 items-end gap-3\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-4\">\n {leadBadge !== undefined && leadBadge !== null && (\n <div className=\"flex flex-wrap items-center gap-2\">{leadBadge}</div>\n )}\n <div className=\"flex flex-col gap-1\">\n {title !== undefined && title !== null && title !== false && (\n <div id={labelledBy} className=\"typography-header-heading-xs text-content-primary\">\n {title}\n </div>\n )}\n <div className=\"flex flex-col gap-2\">\n {description !== undefined && description !== null && description !== false && (\n <p className=\"typography-body-small-14px-regular text-content-primary\">\n {description}\n </p>\n )}\n {secondaryLine !== undefined && secondaryLine !== null && secondaryLine !== false && (\n <p className=\"typography-description-12px-regular text-content-primary\">\n {secondaryLine}\n </p>\n )}\n </div>\n </div>\n {stackedAction !== undefined && stackedAction !== null && (\n <div className=\"flex flex-col items-end self-start\">{stackedAction}</div>\n )}\n {statusRow !== undefined && statusRow !== null && (\n <div className=\"flex flex-wrap items-center gap-2\">{statusRow}</div>\n )}\n </div>\n {primaryAction !== undefined && primaryAction !== null && (\n <div className=\"shrink-0 self-center\">{primaryAction}</div>\n )}\n </div>\n </>\n );\n}\n\ntype InverseBodyProps = Pick<\n BannerProps,\n \"media\" | \"eyebrow\" | \"title\" | \"description\" | \"primaryAction\"\n> & {\n layout: BannerLayout;\n labelledBy?: string;\n dismissSlot: React.ReactNode;\n};\n\nfunction BannerInverseBody({\n layout,\n media,\n eyebrow,\n title,\n description,\n primaryAction,\n labelledBy,\n dismissSlot,\n}: InverseBodyProps) {\n const mediaSizeDefault = \"size-12 shrink-0 overflow-hidden rounded-xl\";\n const titleClassInverse = \"typography-header-heading-xs text-content-primary-inverted\";\n const textColumn = (\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n {eyebrow !== undefined && eyebrow !== null && eyebrow !== false && (\n <p className=\"typography-description-12px-semibold text-content-primary-inverted\">\n {eyebrow}\n </p>\n )}\n {title !== undefined && title !== null && title !== false && (\n <div id={labelledBy} className={titleClassInverse}>\n {title}\n </div>\n )}\n {description !== undefined && description !== null && description !== false && (\n <p className=\"typography-body-small-14px-regular text-content-primary-inverted\">\n {description}\n </p>\n )}\n </div>\n );\n\n return (\n <>\n {media !== undefined && media !== null && <div className={mediaSizeDefault}>{media}</div>}\n {layout === \"horizontal\" ? (\n <div className=\"flex min-w-0 flex-1 items-end gap-3\">\n {textColumn}\n {primaryAction !== undefined && primaryAction !== null && (\n <div className=\"shrink-0\">{primaryAction}</div>\n )}\n </div>\n ) : (\n <div className=\"flex min-w-0 flex-1 flex-col gap-3\">\n {textColumn}\n {primaryAction !== undefined && primaryAction !== null && <div>{primaryAction}</div>}\n </div>\n )}\n {dismissSlot}\n </>\n );\n}\n\nexport const Banner = React.forwardRef<HTMLElement, BannerProps>(\n (\n {\n className,\n variant,\n layout: layoutProp,\n media,\n eyebrow,\n leadBadge,\n title,\n description,\n secondaryLine,\n stackedAction,\n statusRow,\n primaryAction,\n textAction,\n onDismiss,\n dismissLabel = \"Dismiss banner\",\n ...props\n },\n ref,\n ) => {\n const layout = resolveLayout(variant, layoutProp);\n const showDismiss = onDismiss !== undefined && variant === \"Default\";\n const titleId = React.useId();\n const regionLabelledBy =\n title !== undefined && title !== null && title !== false ? titleId : undefined;\n\n const rootClass = bannerRootClass(variant, layout, className);\n\n if (isAppStoreVariant(variant)) {\n return (\n <BannerSection\n ref={ref}\n labelledBy={regionLabelledBy}\n style={guideGradient[variant]}\n className={rootClass}\n {...props}\n >\n <BannerGuideBody\n appStoreVariant={variant}\n eyebrow={eyebrow}\n title={title}\n description={description}\n textAction={textAction}\n labelledBy={regionLabelledBy}\n />\n </BannerSection>\n );\n }\n\n if (variant === \"whatsNew\") {\n return (\n <BannerSection ref={ref} labelledBy={regionLabelledBy} className={rootClass} {...props}>\n <BannerFeatureBody\n layout={layout}\n media={media}\n title={title}\n description={description}\n textAction={textAction}\n labelledBy={regionLabelledBy}\n />\n </BannerSection>\n );\n }\n\n if (variant === \"Subtle\") {\n return (\n <BannerSection ref={ref} labelledBy={regionLabelledBy} className={rootClass} {...props}>\n <BannerSubtleBody\n media={media}\n leadBadge={leadBadge}\n title={title}\n description={description}\n secondaryLine={secondaryLine}\n stackedAction={stackedAction}\n statusRow={statusRow}\n primaryAction={primaryAction}\n labelledBy={regionLabelledBy}\n />\n </BannerSection>\n );\n }\n\n const dismissSlot =\n showDismiss && onDismiss !== undefined ? (\n <BannerDismiss onDismiss={onDismiss} dismissLabel={dismissLabel} />\n ) : null;\n\n return (\n <BannerSection ref={ref} labelledBy={regionLabelledBy} className={rootClass} {...props}>\n <div className=\"flex w-full items-start gap-3\">\n <BannerInverseBody\n layout={layout}\n media={media}\n eyebrow={eyebrow}\n title={title}\n description={description}\n primaryAction={primaryAction}\n labelledBy={regionLabelledBy}\n dismissSlot={dismissSlot}\n />\n </div>\n </BannerSection>\n );\n },\n);\n\nBanner.displayName = \"Banner\";\n"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
|
3
3
|
import { Slot, Slottable } from "@radix-ui/react-slot";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
import { cn } from "../../utils/cn.mjs";
|
|
6
|
+
import { Count } from "../Count/Count.mjs";
|
|
6
7
|
const Chip = React.forwardRef(
|
|
7
8
|
({
|
|
8
9
|
className,
|
|
@@ -15,6 +16,9 @@ const Chip = React.forwardRef(
|
|
|
15
16
|
leftIcon,
|
|
16
17
|
rightIcon,
|
|
17
18
|
notificationLabel,
|
|
19
|
+
notificationCount,
|
|
20
|
+
notificationMax,
|
|
21
|
+
notificationVariant = "brand",
|
|
18
22
|
onClick,
|
|
19
23
|
asChild = false,
|
|
20
24
|
children,
|
|
@@ -110,7 +114,23 @@ const Chip = React.forwardRef(
|
|
|
110
114
|
}
|
|
111
115
|
)
|
|
112
116
|
] }),
|
|
113
|
-
notificationLabel && /* @__PURE__ */ jsx(
|
|
117
|
+
(notificationCount != null || notificationLabel) && /* @__PURE__ */ jsx(
|
|
118
|
+
Count,
|
|
119
|
+
{
|
|
120
|
+
variant: notificationVariant,
|
|
121
|
+
size: "32",
|
|
122
|
+
className: cn(
|
|
123
|
+
// V2 Notification Count: 20px high/min-width, caption 12,
|
|
124
|
+
// 4px horizontal and 2px vertical padding, rounded-md.
|
|
125
|
+
"absolute top-[-4px] rounded-md px-1 py-0.5",
|
|
126
|
+
// Anchor the leading edge so wider values grow away from the label.
|
|
127
|
+
size === "40" ? "left-[calc(100%_-_16px)]" : "left-[calc(100%_-_11px)]"
|
|
128
|
+
),
|
|
129
|
+
value: notificationCount ?? 0,
|
|
130
|
+
max: notificationMax,
|
|
131
|
+
children: notificationCount == null ? notificationLabel : void 0
|
|
132
|
+
}
|
|
133
|
+
)
|
|
114
134
|
] })
|
|
115
135
|
}
|
|
116
136
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chip.mjs","sources":["../../../src/components/Chip/Chip.tsx"],"sourcesContent":["import { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Visual variant of the chip. */\nexport type ChipVariant = \"rounded\" | \"square\" | \"dark\";\n/** Height of the chip in pixels. */\nexport type ChipSize = \"32\" | \"40\";\n\nexport interface ChipProps extends React.HTMLAttributes<HTMLElement> {\n /** Visual variant of the chip. @default \"rounded\" */\n variant?: ChipVariant;\n /** Height of the chip in pixels. @default \"32\" */\n size?: ChipSize;\n /** Whether the chip is in a selected (pressed) state. @default false */\n selected?: boolean;\n /** Whether the chip is disabled. @default false */\n disabled?: boolean;\n /** Whether to show a coloured status dot at the leading edge. @default false */\n leftDot?: boolean;\n /**\n * Whether the chip uses a dashed border for add/create affordances.\n * When `selected`, it becomes a subtle filled state with a solid border.\n * Has no effect when `variant=\"dark\"`.\n * @default false\n */\n dotted?: boolean;\n /** Icon element displayed before the label. */\n leftIcon?: React.ReactNode;\n /** Icon element displayed after the label. */\n rightIcon?: React.ReactNode;\n /** Notification badge content (e.g. `\"99+\"`). Passed as a string for i18n support. */\n notificationLabel?: string;\n /** Click handler — when provided, the chip renders as a `<button>` for accessibility. */\n onClick?: React.MouseEventHandler<HTMLElement>;\n /** Merge props onto a child element instead of rendering a wrapper. @default false */\n asChild?: boolean;\n}\n\n/**\n * A compact element for filters, tags, or toggleable actions. When an `onClick`\n * handler is provided, the chip renders as an interactive `<button>` with\n * `aria-pressed` support.\n *\n * @example\n * ```tsx\n * <Chip selected onClick={toggle}>Music</Chip>\n * ```\n */\nexport const Chip = React.forwardRef<HTMLButtonElement, ChipProps>(\n (\n {\n className,\n variant = \"rounded\",\n size = \"32\",\n selected = false,\n disabled = false,\n leftDot = false,\n dotted = false,\n leftIcon,\n rightIcon,\n notificationLabel,\n onClick,\n asChild = false,\n children,\n ...props\n },\n ref,\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Variant-heavy UI component\n ) => {\n const isInteractive = !!onClick && !asChild;\n const Comp = asChild ? Slot : isInteractive ? \"button\" : \"span\";\n const isDark = variant === \"dark\";\n\n // The dashed border is drawn as an SVG so the dash length matches the design\n // spec (8/8 for the 40px square, 6/6 otherwise). CSS `border-dashed` only\n // renders browser-default dash lengths, which are too short. The stroke colour\n // is driven by `currentColor`, and `group-hover`/`group-active` react to the\n // chip's interactive states. Rendered only in the default (non-`asChild`) path.\n const showDottedBorder = !isDark && dotted && !selected && !asChild;\n const dottedBorder = showDottedBorder ? (\n <svg\n aria-hidden=\"true\"\n className={cn(\n \"pointer-events-none absolute inset-0 size-full overflow-visible\",\n disabled ? \"text-buttons-chip-disabled\" : \"text-buttons-chip-dotted-default\",\n isInteractive &&\n !disabled &&\n \"group-hover:text-buttons-chip-dotted-hover-stroke group-active:text-buttons-chip-dotted-hover-stroke\",\n )}\n >\n <rect\n x=\"0.5\"\n y=\"0.5\"\n rx={variant === \"square\" ? 7.5 : size === \"40\" ? 19.5 : 15.5}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1\"\n strokeDasharray={variant === \"square\" && size === \"40\" ? \"8 8\" : \"6 6\"}\n style={{ width: \"calc(100% - 1px)\", height: \"calc(100% - 1px)\" }}\n />\n </svg>\n ) : null;\n\n return (\n <Comp\n ref={ref}\n data-testid=\"chip\"\n className={cn(\n \"relative inline-flex min-w-0 items-center justify-center whitespace-nowrap motion-safe:transition-colors motion-safe:duration-150\",\n // Shape\n variant === \"square\" ? \"rounded-xs\" : \"rounded-full\",\n // Size (32px uses 12px text, 40px uses 14px text per the design spec)\n size === \"32\" && \"typography-description-12px-semibold h-8 py-1\",\n size === \"40\" && \"typography-body-small-14px-semibold h-10 py-2.5\",\n // Variant colors\n isDark && \"bg-neutral-alphas-600 text-content-always-white\",\n !isDark && selected && !dotted && \"bg-buttons-chip-active text-content-primary-inverted\",\n // Active + dotted is a subtle filled state with a solid (non-dashed) border.\n !isDark &&\n selected &&\n dotted &&\n \"border border-border-primary border-solid bg-inputs-inputs-primary text-content-primary\",\n !isDark && !selected && !dotted && \"bg-buttons-chip-default text-content-primary\",\n // Dotted (non-selected): the dashed border is drawn via SVG (`dottedBorder`).\n // `group` lets that SVG react to hover/active. `asChild` keeps a CSS dashed\n // border fallback since the SVG is only rendered in the default path.\n !isDark && !selected && dotted && \"bg-transparent text-content-primary\",\n !isDark && !selected && dotted && !asChild && \"group\",\n asChild &&\n !isDark &&\n !selected &&\n dotted &&\n (disabled\n ? \"border border-buttons-chip-disabled border-dashed\"\n : \"border border-buttons-chip-dotted-default border-dashed\"),\n // Interactive\n isInteractive && !disabled && \"cursor-pointer\",\n isInteractive &&\n !disabled &&\n !isDark &&\n !selected &&\n !dotted &&\n \"hover:bg-buttons-chip-hover\",\n isInteractive &&\n !disabled &&\n !isDark &&\n !selected &&\n dotted &&\n \"hover:bg-neutral-alphas-50 active:bg-neutral-alphas-50\",\n // Focus\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n // Disabled\n disabled && isDark && \"pointer-events-none opacity-50\",\n disabled && !isDark && \"pointer-events-none text-content-disabled\",\n // Solid (non-dotted) disabled chips get a muted fill; dotted ones stay\n // transparent with their dashed border (drawn via SVG).\n disabled && !isDark && !dotted && \"bg-buttons-chip-disabled\",\n className,\n )}\n {...(isInteractive && {\n type: \"button\" as const,\n disabled,\n \"aria-pressed\": selected,\n onClick,\n })}\n {...(!isInteractive && disabled && { \"aria-disabled\": true })}\n {...(selected && { \"data-selected\": \"\" })}\n {...props}\n >\n {asChild ? (\n <Slottable>{children}</Slottable>\n ) : (\n <>\n {dottedBorder}\n <span className=\"flex min-w-0 items-center gap-0.5 overflow-hidden px-3\">\n {leftDot && (\n <span className=\"size-2 shrink-0 rounded-full bg-current\" aria-hidden=\"true\" />\n )}\n {leftIcon && (\n <span className=\"flex shrink-0 items-center justify-center\" aria-hidden=\"true\">\n {leftIcon}\n </span>\n )}\n <span className=\"min-w-0 truncate\">{children}</span>\n {rightIcon && (\n <span\n className=\"flex size-5 shrink-0 items-center justify-center\"\n aria-hidden=\"true\"\n >\n {rightIcon}\n </span>\n )}\n </span>\n {notificationLabel && (\n <span className=\"typography-description-12px-semibold absolute -top-1 -right-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-content-primary px-1 text-content-primary-inverted\">\n {notificationLabel}\n </span>\n )}\n </>\n )}\n </Comp>\n );\n },\n);\n\nChip.displayName = \"Chip\";\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Chip.mjs","sources":["../../../src/components/Chip/Chip.tsx"],"sourcesContent":["import { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { Count, type CountVariant } from \"../Count/Count\";\n\n/** Visual variant of the chip. */\nexport type ChipVariant = \"rounded\" | \"square\" | \"dark\";\n/** Height of the chip in pixels. */\nexport type ChipSize = \"32\" | \"40\";\n\nexport interface ChipProps extends React.HTMLAttributes<HTMLElement> {\n /** Visual variant of the chip. @default \"rounded\" */\n variant?: ChipVariant;\n /** Height of the chip in pixels. @default \"32\" */\n size?: ChipSize;\n /** Whether the chip is in a selected (pressed) state. @default false */\n selected?: boolean;\n /** Whether the chip is disabled. @default false */\n disabled?: boolean;\n /** Whether to show a coloured status dot at the leading edge. @default false */\n leftDot?: boolean;\n /**\n * Whether the chip uses a dashed border for add/create affordances.\n * When `selected`, it becomes a subtle filled state with a solid border.\n * Has no effect when `variant=\"dark\"`.\n * @default false\n */\n dotted?: boolean;\n /** Icon element displayed before the label. */\n leftIcon?: React.ReactNode;\n /** Icon element displayed after the label. */\n rightIcon?: React.ReactNode;\n /** Notification badge content (e.g. `\"99+\"`). Passed as a string for i18n support. */\n notificationLabel?: string;\n /** Numeric value for the notification badge. Uses the `Count` component for overflow formatting. Takes precedence over `notificationLabel` when both are provided. */\n notificationCount?: number;\n /** Maximum value before the badge shows overflow (e.g. `\"9+\"`). Only applies when `notificationCount` is set. @default 99 */\n notificationMax?: number;\n /** Colour variant of the notification badge. @default \"brand\" */\n notificationVariant?: CountVariant;\n /** Click handler — when provided, the chip renders as a `<button>` for accessibility. */\n onClick?: React.MouseEventHandler<HTMLElement>;\n /** Merge props onto a child element instead of rendering a wrapper. @default false */\n asChild?: boolean;\n}\n\n/**\n * A compact element for filters, tags, or toggleable actions. When an `onClick`\n * handler is provided, the chip renders as an interactive `<button>` with\n * `aria-pressed` support.\n *\n * @example\n * ```tsx\n * <Chip selected onClick={toggle}>Music</Chip>\n * ```\n */\nexport const Chip = React.forwardRef<HTMLButtonElement, ChipProps>(\n (\n {\n className,\n variant = \"rounded\",\n size = \"32\",\n selected = false,\n disabled = false,\n leftDot = false,\n dotted = false,\n leftIcon,\n rightIcon,\n notificationLabel,\n notificationCount,\n notificationMax,\n notificationVariant = \"brand\",\n onClick,\n asChild = false,\n children,\n ...props\n },\n ref,\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Variant-heavy UI component\n ) => {\n const isInteractive = !!onClick && !asChild;\n const Comp = asChild ? Slot : isInteractive ? \"button\" : \"span\";\n const isDark = variant === \"dark\";\n\n // The dashed border is drawn as an SVG so the dash length matches the design\n // spec (8/8 for the 40px square, 6/6 otherwise). CSS `border-dashed` only\n // renders browser-default dash lengths, which are too short. The stroke colour\n // is driven by `currentColor`, and `group-hover`/`group-active` react to the\n // chip's interactive states. Rendered only in the default (non-`asChild`) path.\n const showDottedBorder = !isDark && dotted && !selected && !asChild;\n const dottedBorder = showDottedBorder ? (\n <svg\n aria-hidden=\"true\"\n className={cn(\n \"pointer-events-none absolute inset-0 size-full overflow-visible\",\n disabled ? \"text-buttons-chip-disabled\" : \"text-buttons-chip-dotted-default\",\n isInteractive &&\n !disabled &&\n \"group-hover:text-buttons-chip-dotted-hover-stroke group-active:text-buttons-chip-dotted-hover-stroke\",\n )}\n >\n <rect\n x=\"0.5\"\n y=\"0.5\"\n rx={variant === \"square\" ? 7.5 : size === \"40\" ? 19.5 : 15.5}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1\"\n strokeDasharray={variant === \"square\" && size === \"40\" ? \"8 8\" : \"6 6\"}\n style={{ width: \"calc(100% - 1px)\", height: \"calc(100% - 1px)\" }}\n />\n </svg>\n ) : null;\n\n return (\n <Comp\n ref={ref}\n data-testid=\"chip\"\n className={cn(\n \"relative inline-flex min-w-0 items-center justify-center whitespace-nowrap motion-safe:transition-colors motion-safe:duration-150\",\n // Shape\n variant === \"square\" ? \"rounded-xs\" : \"rounded-full\",\n // Size (32px uses 12px text, 40px uses 14px text per the design spec)\n size === \"32\" && \"typography-description-12px-semibold h-8 py-1\",\n size === \"40\" && \"typography-body-small-14px-semibold h-10 py-2.5\",\n // Variant colors\n isDark && \"bg-neutral-alphas-600 text-content-always-white\",\n !isDark && selected && !dotted && \"bg-buttons-chip-active text-content-primary-inverted\",\n // Active + dotted is a subtle filled state with a solid (non-dashed) border.\n !isDark &&\n selected &&\n dotted &&\n \"border border-border-primary border-solid bg-inputs-inputs-primary text-content-primary\",\n !isDark && !selected && !dotted && \"bg-buttons-chip-default text-content-primary\",\n // Dotted (non-selected): the dashed border is drawn via SVG (`dottedBorder`).\n // `group` lets that SVG react to hover/active. `asChild` keeps a CSS dashed\n // border fallback since the SVG is only rendered in the default path.\n !isDark && !selected && dotted && \"bg-transparent text-content-primary\",\n !isDark && !selected && dotted && !asChild && \"group\",\n asChild &&\n !isDark &&\n !selected &&\n dotted &&\n (disabled\n ? \"border border-buttons-chip-disabled border-dashed\"\n : \"border border-buttons-chip-dotted-default border-dashed\"),\n // Interactive\n isInteractive && !disabled && \"cursor-pointer\",\n isInteractive &&\n !disabled &&\n !isDark &&\n !selected &&\n !dotted &&\n \"hover:bg-buttons-chip-hover\",\n isInteractive &&\n !disabled &&\n !isDark &&\n !selected &&\n dotted &&\n \"hover:bg-neutral-alphas-50 active:bg-neutral-alphas-50\",\n // Focus\n \"focus-visible:shadow-focus-ring focus-visible:outline-none\",\n // Disabled\n disabled && isDark && \"pointer-events-none opacity-50\",\n disabled && !isDark && \"pointer-events-none text-content-disabled\",\n // Solid (non-dotted) disabled chips get a muted fill; dotted ones stay\n // transparent with their dashed border (drawn via SVG).\n disabled && !isDark && !dotted && \"bg-buttons-chip-disabled\",\n className,\n )}\n {...(isInteractive && {\n type: \"button\" as const,\n disabled,\n \"aria-pressed\": selected,\n onClick,\n })}\n {...(!isInteractive && disabled && { \"aria-disabled\": true })}\n {...(selected && { \"data-selected\": \"\" })}\n {...props}\n >\n {asChild ? (\n <Slottable>{children}</Slottable>\n ) : (\n <>\n {dottedBorder}\n <span className=\"flex min-w-0 items-center gap-0.5 overflow-hidden px-3\">\n {leftDot && (\n <span className=\"size-2 shrink-0 rounded-full bg-current\" aria-hidden=\"true\" />\n )}\n {leftIcon && (\n <span className=\"flex shrink-0 items-center justify-center\" aria-hidden=\"true\">\n {leftIcon}\n </span>\n )}\n <span className=\"min-w-0 truncate\">{children}</span>\n {rightIcon && (\n <span\n className=\"flex size-5 shrink-0 items-center justify-center\"\n aria-hidden=\"true\"\n >\n {rightIcon}\n </span>\n )}\n </span>\n {(notificationCount != null || notificationLabel) && (\n <Count\n variant={notificationVariant}\n size=\"32\"\n className={cn(\n // V2 Notification Count: 20px high/min-width, caption 12,\n // 4px horizontal and 2px vertical padding, rounded-md.\n \"absolute top-[-4px] rounded-md px-1 py-0.5\",\n // Anchor the leading edge so wider values grow away from the label.\n size === \"40\" ? \"left-[calc(100%_-_16px)]\" : \"left-[calc(100%_-_11px)]\",\n )}\n value={notificationCount ?? 0}\n max={notificationMax}\n >\n {notificationCount == null ? notificationLabel : undefined}\n </Count>\n )}\n </>\n )}\n </Comp>\n );\n },\n);\n\nChip.displayName = \"Chip\";\n"],"names":[],"mappings":"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"}
|
|
@@ -6,12 +6,33 @@ import { cn } from "../../utils/cn.mjs";
|
|
|
6
6
|
function getDisplayValue(value, max) {
|
|
7
7
|
return value > max ? `${max}+` : value.toString();
|
|
8
8
|
}
|
|
9
|
+
function variantClasses(variant) {
|
|
10
|
+
switch (variant) {
|
|
11
|
+
case "default":
|
|
12
|
+
return "bg-content-primary text-content-primary-inverted";
|
|
13
|
+
case "contrast":
|
|
14
|
+
return "bg-content-always-white text-content-always-black";
|
|
15
|
+
case "brand":
|
|
16
|
+
return "bg-brand-primary-default text-content-always-black";
|
|
17
|
+
case "alert":
|
|
18
|
+
return "bg-error-content text-content-always-white";
|
|
19
|
+
case "pink":
|
|
20
|
+
return "bg-brand-secondary-default text-content-always-black";
|
|
21
|
+
case "info":
|
|
22
|
+
return "bg-info-content text-content-always-white";
|
|
23
|
+
case "success":
|
|
24
|
+
return "bg-success-content text-content-always-white";
|
|
25
|
+
case "warning":
|
|
26
|
+
return "bg-warning-content text-content-always-black";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
9
29
|
const Count = React.forwardRef(
|
|
10
30
|
({
|
|
11
31
|
className,
|
|
12
32
|
variant = "default",
|
|
13
33
|
value = 0,
|
|
14
34
|
max = 99,
|
|
35
|
+
showAmount = true,
|
|
15
36
|
size = "32",
|
|
16
37
|
asChild = false,
|
|
17
38
|
children,
|
|
@@ -21,22 +42,30 @@ const Count = React.forwardRef(
|
|
|
21
42
|
return null;
|
|
22
43
|
}
|
|
23
44
|
const Comp = asChild ? Slot : "span";
|
|
45
|
+
const colors = variantClasses(variant);
|
|
46
|
+
if (!showAmount) {
|
|
47
|
+
return /* @__PURE__ */ jsx(
|
|
48
|
+
Comp,
|
|
49
|
+
{
|
|
50
|
+
ref,
|
|
51
|
+
"data-testid": "count",
|
|
52
|
+
className: cn("inline-flex size-2 shrink-0 rounded-full", colors, className),
|
|
53
|
+
...props
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
}
|
|
24
57
|
return /* @__PURE__ */ jsx(
|
|
25
58
|
Comp,
|
|
26
59
|
{
|
|
27
60
|
ref,
|
|
61
|
+
"data-testid": "count",
|
|
28
62
|
className: cn(
|
|
29
63
|
"typography-description-12px-semibold inline-flex shrink-0 items-center justify-center rounded-full tabular-nums leading-none",
|
|
30
64
|
size === "16" && "h-3 min-w-3 px-0.5 text-[8px]",
|
|
65
|
+
// Kept at 16px so nav / IconButton badges don't grow; Chip nests this size.
|
|
31
66
|
size === "24" && "h-4 min-w-4 px-1 text-[10px]",
|
|
32
67
|
size === "32" && "h-5 min-w-5 px-1.5 text-[12px]",
|
|
33
|
-
|
|
34
|
-
variant === "alert" && "bg-error-content text-content-always-white",
|
|
35
|
-
variant === "brand" && "bg-brand-primary-default text-content-always-black",
|
|
36
|
-
variant === "pink" && "bg-brand-secondary-default text-content-always-black",
|
|
37
|
-
variant === "info" && "bg-info-content text-content-always-white",
|
|
38
|
-
variant === "success" && "bg-success-content text-content-always-white",
|
|
39
|
-
variant === "warning" && "bg-warning-content text-content-always-black",
|
|
68
|
+
colors,
|
|
40
69
|
className
|
|
41
70
|
),
|
|
42
71
|
...props,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Count.mjs","sources":["../../../src/components/Count/Count.tsx"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n
|
|
1
|
+
{"version":3,"file":"Count.mjs","sources":["../../../src/components/Count/Count.tsx"],"sourcesContent":["import { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/**\n * Colour variant for the count badge.\n *\n * V2 Notification Count (Fanvue Library) types: `default`, `brand`, `contrast`.\n * Remaining values are retained extensions for existing consumers.\n */\nexport type CountVariant =\n | \"default\"\n | \"contrast\"\n | \"brand\"\n | \"alert\"\n | \"pink\"\n | \"info\"\n | \"success\"\n | \"warning\";\n\n/** Size of the count badge, aligned with button and icon-button sizes. */\nexport type CountSize = \"16\" | \"24\" | \"32\";\n\nfunction getDisplayValue(value: number, max: number): string {\n return value > max ? `${max}+` : value.toString();\n}\n\nfunction variantClasses(variant: CountVariant): string {\n switch (variant) {\n case \"default\":\n return \"bg-content-primary text-content-primary-inverted\";\n case \"contrast\":\n // V2: on dark or coloured hosts (filled buttons, images)\n return \"bg-content-always-white text-content-always-black\";\n case \"brand\":\n return \"bg-brand-primary-default text-content-always-black\";\n case \"alert\":\n return \"bg-error-content text-content-always-white\";\n case \"pink\":\n return \"bg-brand-secondary-default text-content-always-black\";\n case \"info\":\n return \"bg-info-content text-content-always-white\";\n case \"success\":\n return \"bg-success-content text-content-always-white\";\n case \"warning\":\n return \"bg-warning-content text-content-always-black\";\n }\n}\n\nexport interface CountProps extends React.HTMLAttributes<HTMLSpanElement> {\n /**\n * Colour variant of the count badge.\n * Figma V2 types: `default` | `brand` | `contrast`. @default \"default\"\n */\n variant?: CountVariant;\n /** Numeric value to display. Renders nothing when `0` and no `children` are provided. @default 0 */\n value?: number;\n /** Maximum value before showing overflow (e.g. `\"99+\"`). @default 99 */\n max?: number;\n /**\n * When `false`, renders an 8px unread dot instead of a numeric amount\n * (Figma Show Amount=False). @default true\n */\n showAmount?: boolean;\n /** Size of the count badge. @default \"32\" */\n size?: CountSize;\n /** Merge props onto a child element instead of rendering a `<span>`. @default false */\n asChild?: boolean;\n}\n\n/**\n * V2 Notification Count — a small badge for unread activity on nav items,\n * icons, buttons, or chips. Truncates values above `max` (e.g. `\"99+\"`).\n * Renders nothing when the value is `0` and no children are provided.\n *\n * @example\n * ```tsx\n * <Count value={5} variant=\"brand\" />\n * <Count value={1} variant=\"brand\" showAmount={false} />\n * ```\n */\nexport const Count = React.forwardRef<HTMLSpanElement, CountProps>(\n (\n {\n className,\n variant = \"default\",\n value = 0,\n max = 99,\n showAmount = true,\n size = \"32\",\n asChild = false,\n children,\n ...props\n },\n ref,\n ) => {\n if (value === 0 && !children) {\n return null;\n }\n\n const Comp = asChild ? Slot : \"span\";\n const colors = variantClasses(variant);\n\n if (!showAmount) {\n return (\n <Comp\n ref={ref}\n data-testid=\"count\"\n className={cn(\"inline-flex size-2 shrink-0 rounded-full\", colors, className)}\n {...props}\n />\n );\n }\n\n return (\n <Comp\n ref={ref}\n data-testid=\"count\"\n className={cn(\n \"typography-description-12px-semibold inline-flex shrink-0 items-center justify-center rounded-full tabular-nums leading-none\",\n size === \"16\" && \"h-3 min-w-3 px-0.5 text-[8px]\",\n // Kept at 16px so nav / IconButton badges don't grow; Chip nests this size.\n size === \"24\" && \"h-4 min-w-4 px-1 text-[10px]\",\n size === \"32\" && \"h-5 min-w-5 px-1.5 text-[12px]\",\n colors,\n className,\n )}\n {...props}\n >\n {children ?? getDisplayValue(value, max)}\n </Comp>\n );\n },\n);\n\nCount.displayName = \"Count\";\n"],"names":[],"mappings":";;;;;AAuBA,SAAS,gBAAgB,OAAe,KAAqB;AAC3D,SAAO,QAAQ,MAAM,GAAG,GAAG,MAAM,MAAM,SAAA;AACzC;AAEA,SAAS,eAAe,SAA+B;AACrD,UAAQ,SAAA;AAAA,IACN,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAEH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,EAAA;AAEb;AAkCO,MAAM,QAAQ,MAAM;AAAA,EACzB,CACE;AAAA,IACE;AAAA,IACA,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,aAAa;AAAA,IACb,OAAO;AAAA,IACP,UAAU;AAAA,IACV;AAAA,IACA,GAAG;AAAA,EAAA,GAEL,QACG;AACH,QAAI,UAAU,KAAK,CAAC,UAAU;AAC5B,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,UAAU,OAAO;AAC9B,UAAM,SAAS,eAAe,OAAO;AAErC,QAAI,CAAC,YAAY;AACf,aACE;AAAA,QAAC;AAAA,QAAA;AAAA,UACC;AAAA,UACA,eAAY;AAAA,UACZ,WAAW,GAAG,4CAA4C,QAAQ,SAAS;AAAA,UAC1E,GAAG;AAAA,QAAA;AAAA,MAAA;AAAA,IAGV;AAEA,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,eAAY;AAAA,QACZ,WAAW;AAAA,UACT;AAAA,UACA,SAAS,QAAQ;AAAA;AAAA,UAEjB,SAAS,QAAQ;AAAA,UACjB,SAAS,QAAQ;AAAA,UACjB;AAAA,UACA;AAAA,QAAA;AAAA,QAED,GAAG;AAAA,QAEH,UAAA,YAAY,gBAAgB,OAAO,GAAG;AAAA,MAAA;AAAA,IAAA;AAAA,EAG7C;AACF;AAEA,MAAM,cAAc;"}
|
|
@@ -89,7 +89,10 @@ const CyclingText = React.forwardRef(
|
|
|
89
89
|
{
|
|
90
90
|
ref: sizingLabelRef,
|
|
91
91
|
"aria-hidden": "true",
|
|
92
|
-
className:
|
|
92
|
+
className: cn(
|
|
93
|
+
"pointer-events-none invisible inline-block select-none whitespace-nowrap leading-[inherit]",
|
|
94
|
+
labelClassName
|
|
95
|
+
),
|
|
93
96
|
children: sizingLabel
|
|
94
97
|
}
|
|
95
98
|
),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CyclingText.mjs","sources":["../../../src/components/CyclingText/CyclingText.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useCyclingCycle } from \"./useCyclingCycle\";\nimport { useCyclingTextTrackWidth } from \"./useCyclingTextTrackWidth\";\nimport { usePageVisibility } from \"./usePageVisibility\";\nimport { usePrefersReducedMotion } from \"./usePrefersReducedMotion\";\n\nconst DEFAULT_INTERVAL_MS = 2100;\nconst DEFAULT_TRANSITION_MS = 200;\n\nconst SLIDE_OFFSET_PX = 18;\n\n/** How the wrapper should be sized to accommodate variable-length items. */\nexport type CyclingTextSizing = \"longest\" | \"current\";\n\nexport interface CyclingTextProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** Strings to cycle through, in order. Cycles back to the first after the last. */\n items: readonly string[];\n /**\n * Milliseconds to wait after the previous transition finishes before starting the next one.\n * @default 2100\n */\n intervalMs?: number;\n /** Slide and cross-fade duration in milliseconds. @default 200 */\n transitionMs?: number;\n /** Direction the outgoing item slides. @default \"up\" */\n direction?: \"up\" | \"down\";\n /** When true, freezes on the current item — no further cycling until cleared. @default false */\n paused?: boolean;\n /**\n * How the wrapper sizes itself horizontally.\n * - `longest` reserves space for the longest item — no width jitter as items cycle.\n * - `current` shrinks/grows with each item, animating width between cycles.\n * @default \"longest\"\n */\n sizing?: CyclingTextSizing;\n /**\n * When `true`, updates are exposed to assistive technologies via `aria-live=\"polite\"`.\n * Leave `false` for decorative or frequently changing copy so screen readers are not interrupted on every cycle.\n * @default false\n */\n announceChanges?: boolean;\n /**\n * Class applied to each visible label span (current + incoming). Use this for\n * effects that have to sit on the text element itself, e.g. `background-clip: text`.\n */\n labelClassName?: string;\n /**\n * Called with the index of the settled, fully-visible item — once on mount and\n * again whenever it changes (after each transition completes). Lets a parent\n * stay in lockstep with what is on screen (e.g. to act on the item the user is\n * currently looking at) without running a second, drifting timer of its own.\n */\n onActiveIndexChange?: (index: number) => void;\n}\n\n/**\n * Cycles through a list of strings with a slide-in/slide-out animation. Lives\n * inline so it can sit inside divs, spans, buttons, or as a fake placeholder\n * overlay.\n *\n * @example\n * ```tsx\n * <CyclingText items={[\"Thinking\", \"Reading messages\", \"Drafting reply\"]} />\n * ```\n */\nexport const CyclingText = React.forwardRef<HTMLSpanElement, CyclingTextProps>(\n (\n {\n items,\n intervalMs = DEFAULT_INTERVAL_MS,\n transitionMs = DEFAULT_TRANSITION_MS,\n direction = \"up\",\n paused = false,\n sizing = \"longest\",\n className,\n labelClassName,\n announceChanges = false,\n onActiveIndexChange,\n ...rest\n },\n ref,\n ) => {\n const docVisible = usePageVisibility();\n const reducedMotion = usePrefersReducedMotion();\n const { sizingLabelRef, trackWidth } = useCyclingTextTrackWidth();\n const {\n cycle,\n currentIndex,\n currentLabel,\n incomingLabel,\n sizingLabel,\n onOutgoingTransitionEnd,\n } = useCyclingCycle(items, sizing, intervalMs, paused, docVisible, transitionMs);\n\n const itemCount = items.length;\n\n // Notify the parent of the settled active index without re-subscribing when\n // an inline callback identity changes each render — the latest is always\n // read from the ref.\n const onActiveIndexChangeRef = React.useRef(onActiveIndexChange);\n React.useEffect(() => {\n onActiveIndexChangeRef.current = onActiveIndexChange;\n }, [onActiveIndexChange]);\n React.useEffect(() => {\n onActiveIndexChangeRef.current?.(currentIndex);\n }, [currentIndex]);\n\n const outgoingMotionStyle = React.useMemo((): React.CSSProperties => {\n const durMs = reducedMotion ? 0 : transitionMs;\n const exiting = cycle.transitioning;\n const yExit = direction === \"up\" ? -SLIDE_OFFSET_PX : SLIDE_OFFSET_PX;\n return {\n opacity: exiting ? 0 : 1,\n transform: exiting ? `translate3d(0, ${yExit}px, 0)` : \"translate3d(0, 0, 0)\",\n transition:\n exiting && durMs > 0\n ? `opacity ${durMs}ms cubic-bezier(0.4, 0, 0.2, 1), transform ${durMs}ms cubic-bezier(0.4, 0, 0.2, 1)`\n : \"none\",\n };\n }, [cycle.transitioning, direction, transitionMs, reducedMotion]);\n\n const incomingMotionStyle = React.useMemo((): React.CSSProperties => {\n const durMs = reducedMotion ? 0 : transitionMs;\n const entered = cycle.incomingEntered;\n const yEnter = direction === \"up\" ? SLIDE_OFFSET_PX : -SLIDE_OFFSET_PX;\n return {\n opacity: entered ? 1 : 0,\n transform: entered ? \"translate3d(0, 0, 0)\" : `translate3d(0, ${yEnter}px, 0)`,\n transition:\n durMs > 0\n ? `opacity ${durMs}ms cubic-bezier(0.4, 0, 0.2, 1), transform ${durMs}ms cubic-bezier(0.4, 0, 0.2, 1)`\n : \"none\",\n };\n }, [cycle.incomingEntered, direction, transitionMs, reducedMotion]);\n\n if (itemCount === 0) {\n return null;\n }\n\n const wrapperStyle = {\n ...(trackWidth !== null ? { width: `${trackWidth}px` } : {}),\n // paddingTop: SLIDE_OFFSET_PX,\n } as React.CSSProperties;\n\n const showIncoming = incomingLabel !== null && cycle.transitioning;\n\n return (\n <span\n ref={ref}\n data-testid=\"cycling-text\"\n data-paused={paused ? \"true\" : undefined}\n className={cn(\n \"relative inline-flex items-center overflow-hidden align-middle leading-[inherit]\",\n \"motion-safe:transition-[width] motion-safe:duration-300 motion-safe:ease-out\",\n className,\n )}\n style={wrapperStyle}\n {...rest}\n >\n <span\n ref={sizingLabelRef}\n aria-hidden=\"true\"\n className=\"pointer-events-none invisible inline-block select-none whitespace-nowrap leading-[inherit]\"\n >\n {sizingLabel}\n </span>\n\n <span\n data-layer=\"current\"\n aria-live={announceChanges ? \"polite\" : undefined}\n aria-atomic={announceChanges ? true : undefined}\n data-state={cycle.transitioning ? \"exit\" : \"idle\"}\n onTransitionEnd={onOutgoingTransitionEnd}\n className={cn(\n \"absolute inset-0 flex items-center whitespace-nowrap leading-[inherit]\",\n labelClassName,\n )}\n style={outgoingMotionStyle}\n >\n {currentLabel}\n </span>\n\n {showIncoming ? (\n <span\n aria-hidden=\"true\"\n data-layer=\"incoming\"\n data-state={cycle.incomingEntered ? \"idle\" : \"enter\"}\n className={cn(\n \"absolute inset-0 flex items-center whitespace-nowrap leading-[inherit]\",\n labelClassName,\n )}\n style={incomingMotionStyle}\n >\n {incomingLabel}\n </span>\n ) : null}\n </span>\n );\n },\n);\n\nCyclingText.displayName = \"CyclingText\";\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CyclingText.mjs","sources":["../../../src/components/CyclingText/CyclingText.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { useCyclingCycle } from \"./useCyclingCycle\";\nimport { useCyclingTextTrackWidth } from \"./useCyclingTextTrackWidth\";\nimport { usePageVisibility } from \"./usePageVisibility\";\nimport { usePrefersReducedMotion } from \"./usePrefersReducedMotion\";\n\nconst DEFAULT_INTERVAL_MS = 2100;\nconst DEFAULT_TRANSITION_MS = 200;\n\nconst SLIDE_OFFSET_PX = 18;\n\n/** How the wrapper should be sized to accommodate variable-length items. */\nexport type CyclingTextSizing = \"longest\" | \"current\";\n\nexport interface CyclingTextProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\"> {\n /** Strings to cycle through, in order. Cycles back to the first after the last. */\n items: readonly string[];\n /**\n * Milliseconds to wait after the previous transition finishes before starting the next one.\n * @default 2100\n */\n intervalMs?: number;\n /** Slide and cross-fade duration in milliseconds. @default 200 */\n transitionMs?: number;\n /** Direction the outgoing item slides. @default \"up\" */\n direction?: \"up\" | \"down\";\n /** When true, freezes on the current item — no further cycling until cleared. @default false */\n paused?: boolean;\n /**\n * How the wrapper sizes itself horizontally.\n * - `longest` reserves space for the longest item — no width jitter as items cycle.\n * - `current` shrinks/grows with each item, animating width between cycles.\n * @default \"longest\"\n */\n sizing?: CyclingTextSizing;\n /**\n * When `true`, updates are exposed to assistive technologies via `aria-live=\"polite\"`.\n * Leave `false` for decorative or frequently changing copy so screen readers are not interrupted on every cycle.\n * @default false\n */\n announceChanges?: boolean;\n /**\n * Class applied to each visible label span (current + incoming). Use this for\n * effects that have to sit on the text element itself, e.g. `background-clip: text`.\n */\n labelClassName?: string;\n /**\n * Called with the index of the settled, fully-visible item — once on mount and\n * again whenever it changes (after each transition completes). Lets a parent\n * stay in lockstep with what is on screen (e.g. to act on the item the user is\n * currently looking at) without running a second, drifting timer of its own.\n */\n onActiveIndexChange?: (index: number) => void;\n}\n\n/**\n * Cycles through a list of strings with a slide-in/slide-out animation. Lives\n * inline so it can sit inside divs, spans, buttons, or as a fake placeholder\n * overlay.\n *\n * @example\n * ```tsx\n * <CyclingText items={[\"Thinking\", \"Reading messages\", \"Drafting reply\"]} />\n * ```\n */\nexport const CyclingText = React.forwardRef<HTMLSpanElement, CyclingTextProps>(\n (\n {\n items,\n intervalMs = DEFAULT_INTERVAL_MS,\n transitionMs = DEFAULT_TRANSITION_MS,\n direction = \"up\",\n paused = false,\n sizing = \"longest\",\n className,\n labelClassName,\n announceChanges = false,\n onActiveIndexChange,\n ...rest\n },\n ref,\n ) => {\n const docVisible = usePageVisibility();\n const reducedMotion = usePrefersReducedMotion();\n const { sizingLabelRef, trackWidth } = useCyclingTextTrackWidth();\n const {\n cycle,\n currentIndex,\n currentLabel,\n incomingLabel,\n sizingLabel,\n onOutgoingTransitionEnd,\n } = useCyclingCycle(items, sizing, intervalMs, paused, docVisible, transitionMs);\n\n const itemCount = items.length;\n\n // Notify the parent of the settled active index without re-subscribing when\n // an inline callback identity changes each render — the latest is always\n // read from the ref.\n const onActiveIndexChangeRef = React.useRef(onActiveIndexChange);\n React.useEffect(() => {\n onActiveIndexChangeRef.current = onActiveIndexChange;\n }, [onActiveIndexChange]);\n React.useEffect(() => {\n onActiveIndexChangeRef.current?.(currentIndex);\n }, [currentIndex]);\n\n const outgoingMotionStyle = React.useMemo((): React.CSSProperties => {\n const durMs = reducedMotion ? 0 : transitionMs;\n const exiting = cycle.transitioning;\n const yExit = direction === \"up\" ? -SLIDE_OFFSET_PX : SLIDE_OFFSET_PX;\n return {\n opacity: exiting ? 0 : 1,\n transform: exiting ? `translate3d(0, ${yExit}px, 0)` : \"translate3d(0, 0, 0)\",\n transition:\n exiting && durMs > 0\n ? `opacity ${durMs}ms cubic-bezier(0.4, 0, 0.2, 1), transform ${durMs}ms cubic-bezier(0.4, 0, 0.2, 1)`\n : \"none\",\n };\n }, [cycle.transitioning, direction, transitionMs, reducedMotion]);\n\n const incomingMotionStyle = React.useMemo((): React.CSSProperties => {\n const durMs = reducedMotion ? 0 : transitionMs;\n const entered = cycle.incomingEntered;\n const yEnter = direction === \"up\" ? SLIDE_OFFSET_PX : -SLIDE_OFFSET_PX;\n return {\n opacity: entered ? 1 : 0,\n transform: entered ? \"translate3d(0, 0, 0)\" : `translate3d(0, ${yEnter}px, 0)`,\n transition:\n durMs > 0\n ? `opacity ${durMs}ms cubic-bezier(0.4, 0, 0.2, 1), transform ${durMs}ms cubic-bezier(0.4, 0, 0.2, 1)`\n : \"none\",\n };\n }, [cycle.incomingEntered, direction, transitionMs, reducedMotion]);\n\n if (itemCount === 0) {\n return null;\n }\n\n const wrapperStyle = {\n ...(trackWidth !== null ? { width: `${trackWidth}px` } : {}),\n // paddingTop: SLIDE_OFFSET_PX,\n } as React.CSSProperties;\n\n const showIncoming = incomingLabel !== null && cycle.transitioning;\n\n return (\n <span\n ref={ref}\n data-testid=\"cycling-text\"\n data-paused={paused ? \"true\" : undefined}\n className={cn(\n \"relative inline-flex items-center overflow-hidden align-middle leading-[inherit]\",\n \"motion-safe:transition-[width] motion-safe:duration-300 motion-safe:ease-out\",\n className,\n )}\n style={wrapperStyle}\n {...rest}\n >\n <span\n ref={sizingLabelRef}\n aria-hidden=\"true\"\n className={cn(\n \"pointer-events-none invisible inline-block select-none whitespace-nowrap leading-[inherit]\",\n labelClassName,\n )}\n >\n {sizingLabel}\n </span>\n\n <span\n data-layer=\"current\"\n aria-live={announceChanges ? \"polite\" : undefined}\n aria-atomic={announceChanges ? true : undefined}\n data-state={cycle.transitioning ? \"exit\" : \"idle\"}\n onTransitionEnd={onOutgoingTransitionEnd}\n className={cn(\n \"absolute inset-0 flex items-center whitespace-nowrap leading-[inherit]\",\n labelClassName,\n )}\n style={outgoingMotionStyle}\n >\n {currentLabel}\n </span>\n\n {showIncoming ? (\n <span\n aria-hidden=\"true\"\n data-layer=\"incoming\"\n data-state={cycle.incomingEntered ? \"idle\" : \"enter\"}\n className={cn(\n \"absolute inset-0 flex items-center whitespace-nowrap leading-[inherit]\",\n labelClassName,\n )}\n style={incomingMotionStyle}\n >\n {incomingLabel}\n </span>\n ) : null}\n </span>\n );\n },\n);\n\nCyclingText.displayName = \"CyclingText\";\n"],"names":[],"mappings":"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"}
|
|
@@ -25,7 +25,7 @@ const PREFIX_TYPOGRAPHY = {
|
|
|
25
25
|
};
|
|
26
26
|
function getContainerClassName(size, error, disabled) {
|
|
27
27
|
return cn(
|
|
28
|
-
"relative flex items-center gap-2 overflow-hidden rounded-sm border bg-inputs-inputs-primary
|
|
28
|
+
"relative flex items-center gap-2 overflow-hidden rounded-sm border bg-inputs-inputs-primary after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] has-focus-visible:outline-none has-focus-visible:after:shadow-focus-ring motion-safe:transition-colors",
|
|
29
29
|
CONTAINER_PADDING_X[size],
|
|
30
30
|
CONTAINER_HEIGHT[size],
|
|
31
31
|
error ? "border-error-content" : "border-border-primary",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PhoneField.mjs","sources":["../../../src/components/PhoneField/PhoneField.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { ChevronDownIcon } from \"../Icons/ChevronDownIcon\";\n\n/** Phone field height in pixels. */\nexport type PhoneFieldSize = \"48\" | \"40\" | \"32\";\n\nexport interface PhoneFieldProps\n extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"size\" | \"prefix\"> {\n /** Label text displayed above the input. Also used as the accessible name. */\n label?: string;\n /** Helper text displayed below the input. Replaced by `errorMessage` when `error` is `true`. */\n helperText?: string;\n /** Height of the phone field in pixels. @default \"48\" */\n size?: PhoneFieldSize;\n /** Whether the field is in an error state. @default false */\n error?: boolean;\n /** Error message displayed below the input. Shown instead of `helperText` when `error` is `true`. */\n errorMessage?: string;\n /** Dial code of the selected country, shown before the number (e.g. `\"+39\"`). */\n dialCode?: React.ReactNode;\n /** Flag of the selected country, rendered in the country selector (e.g. an image or flag icon). */\n flag?: React.ReactNode;\n /** Fired when the country selector (flag + chevron) is activated. Open the country picker here. */\n onCountrySelect?: () => void;\n /** Accessible name for the country selector button. @default \"Select country\" */\n countryButtonLabel?: string;\n /** Whether the field stretches to fill its container width. @default false */\n fullWidth?: boolean;\n}\n\nconst CONTAINER_HEIGHT: Record<PhoneFieldSize, string> = {\n \"48\": \"h-12\",\n \"40\": \"h-10\",\n \"32\": \"h-8\",\n};\n\nconst CONTAINER_PADDING_X: Record<PhoneFieldSize, string> = {\n \"48\": \"px-4\",\n \"40\": \"px-4\",\n \"32\": \"px-3\",\n};\n\nconst INPUT_TYPOGRAPHY: Record<PhoneFieldSize, string> = {\n \"48\": \"typography-body-default-16px-regular autofill-body-lg\",\n \"40\": \"typography-body-default-16px-regular autofill-body-lg\",\n \"32\": \"typography-body-small-14px-regular autofill-body-md\",\n};\n\nconst PREFIX_TYPOGRAPHY: Record<PhoneFieldSize, string> = {\n \"48\": \"typography-body-default-16px-regular\",\n \"40\": \"typography-body-default-16px-regular\",\n \"32\": \"typography-body-small-14px-regular\",\n};\n\nfunction getContainerClassName(size: PhoneFieldSize, error: boolean, disabled?: boolean) {\n return cn(\n \"relative flex items-center gap-2 overflow-hidden rounded-sm border bg-inputs-inputs-primary has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors\",\n CONTAINER_PADDING_X[size],\n CONTAINER_HEIGHT[size],\n error ? \"border-error-content\" : \"border-border-primary\",\n !disabled && !error && \"hover:border-neutral-alphas-400\",\n disabled && \"opacity-50\",\n );\n}\n\nfunction warnMissingAccessibleName(label?: string, ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!label && !ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"PhoneField: no accessible name provided. Pass a `label`, `aria-label`, or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\nfunction PhoneFieldHelperText({\n id,\n error,\n children,\n}: {\n id: string;\n error: boolean;\n children: React.ReactNode;\n}) {\n return (\n <p\n id={id}\n className={cn(\n \"typography-description-12px-regular pt-2\",\n error ? \"text-error-content\" : \"text-content-tertiary\",\n )}\n >\n {children}\n </p>\n );\n}\n\n/**\n * A phone number input with a country selector (flag + chevron), a fixed dial\n * code prefix, and a `tel` input. The country picker itself is owned by the\n * consumer: pass the selected country's `flag` and `dialCode`, and open your\n * picker from `onCountrySelect`.\n *\n * Provide at least one of `label`, `aria-label`, or `aria-labelledby` for\n * accessibility — a console warning is emitted in development if none are set.\n *\n * @example\n * ```tsx\n * <PhoneField\n * label=\"Phone number\"\n * flag={<img src={italyFlag} alt=\"\" />}\n * dialCode=\"+39\"\n * onCountrySelect={openCountryPicker}\n * value={number}\n * onChange={(e) => setNumber(e.target.value)}\n * />\n * ```\n */\nexport const PhoneField = React.forwardRef<HTMLInputElement, PhoneFieldProps>(\n (\n {\n label,\n helperText,\n size = \"48\",\n error = false,\n errorMessage,\n dialCode,\n flag,\n onCountrySelect,\n countryButtonLabel = \"Select country\",\n className,\n id,\n disabled,\n fullWidth = false,\n type = \"tel\",\n ...props\n },\n ref,\n ) => {\n const generatedId = React.useId();\n const inputId = id || generatedId;\n const helperTextId = `${inputId}-helper`;\n const dialCodeId = `${inputId}-dial-code`;\n const bottomText = error && errorMessage ? errorMessage : helperText;\n\n const describedBy =\n [dialCode != null ? dialCodeId : null, bottomText ? helperTextId : null]\n .filter(Boolean)\n .join(\" \") || undefined;\n\n const innerRef = React.useRef<HTMLInputElement>(null);\n const setRefs = React.useCallback(\n (node: HTMLInputElement | null) => {\n innerRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as React.RefObject<HTMLInputElement | null>).current = node;\n },\n [ref],\n );\n\n const handleContainerMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {\n if (disabled) return;\n const target = event.target as HTMLElement;\n if (target === innerRef.current) return;\n if (target.closest(\"[data-pf-interactive]\")) return;\n event.preventDefault();\n innerRef.current?.focus();\n };\n\n warnMissingAccessibleName(label, props[\"aria-label\"], props[\"aria-labelledby\"]);\n\n return (\n <div\n className={cn(\"flex flex-col\", fullWidth && \"w-full\", className)}\n data-disabled={disabled ? \"\" : undefined}\n data-error={error ? \"\" : undefined}\n >\n {label && (\n <label\n htmlFor={inputId}\n className=\"typography-description-12px-semibold pb-2 text-content-primary\"\n >\n {label}\n </label>\n )}\n\n {/* biome-ignore lint/a11y/noStaticElementInteractions: focus bridge delegates pointer clicks on the padding to the input */}\n <div\n className={getContainerClassName(size, error, disabled)}\n onMouseDown={handleContainerMouseDown}\n >\n <button\n type=\"button\"\n data-pf-interactive=\"\"\n onClick={onCountrySelect}\n disabled={disabled}\n aria-label={countryButtonLabel}\n className=\"flex shrink-0 items-center gap-1 rounded-2xs focus-visible:shadow-focus-ring focus-visible:outline-none disabled:cursor-not-allowed\"\n >\n {flag != null && (\n <span className=\"flex size-6 shrink-0 items-center justify-center overflow-hidden rounded-full\">\n {flag}\n </span>\n )}\n <ChevronDownIcon size={16} className=\"shrink-0 text-content-secondary\" />\n </button>\n\n {dialCode != null && (\n <span\n id={dialCodeId}\n className={cn(\n \"shrink-0 select-none whitespace-nowrap text-content-primary\",\n PREFIX_TYPOGRAPHY[size],\n )}\n >\n {dialCode}\n </span>\n )}\n\n <input\n ref={setRefs}\n id={inputId}\n type={type}\n inputMode=\"tel\"\n disabled={disabled}\n aria-describedby={describedBy}\n aria-invalid={error || undefined}\n className={cn(\n \"h-full min-w-0 flex-1 bg-transparent text-content-primary no-underline placeholder:text-content-tertiary focus:outline-none disabled:cursor-not-allowed\",\n INPUT_TYPOGRAPHY[size],\n )}\n {...props}\n />\n </div>\n\n {bottomText && (\n <PhoneFieldHelperText id={helperTextId} error={error}>\n {bottomText}\n </PhoneFieldHelperText>\n )}\n </div>\n );\n },\n);\n\nPhoneField.displayName = \"PhoneField\";\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PhoneField.mjs","sources":["../../../src/components/PhoneField/PhoneField.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { ChevronDownIcon } from \"../Icons/ChevronDownIcon\";\n\n/** Phone field height in pixels. */\nexport type PhoneFieldSize = \"48\" | \"40\" | \"32\";\n\nexport interface PhoneFieldProps\n extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"size\" | \"prefix\"> {\n /** Label text displayed above the input. Also used as the accessible name. */\n label?: string;\n /** Helper text displayed below the input. Replaced by `errorMessage` when `error` is `true`. */\n helperText?: string;\n /** Height of the phone field in pixels. @default \"48\" */\n size?: PhoneFieldSize;\n /** Whether the field is in an error state. @default false */\n error?: boolean;\n /** Error message displayed below the input. Shown instead of `helperText` when `error` is `true`. */\n errorMessage?: string;\n /** Dial code of the selected country, shown before the number (e.g. `\"+39\"`). */\n dialCode?: React.ReactNode;\n /** Flag of the selected country, rendered in the country selector (e.g. an image or flag icon). */\n flag?: React.ReactNode;\n /** Fired when the country selector (flag + chevron) is activated. Open the country picker here. */\n onCountrySelect?: () => void;\n /** Accessible name for the country selector button. @default \"Select country\" */\n countryButtonLabel?: string;\n /** Whether the field stretches to fill its container width. @default false */\n fullWidth?: boolean;\n}\n\nconst CONTAINER_HEIGHT: Record<PhoneFieldSize, string> = {\n \"48\": \"h-12\",\n \"40\": \"h-10\",\n \"32\": \"h-8\",\n};\n\nconst CONTAINER_PADDING_X: Record<PhoneFieldSize, string> = {\n \"48\": \"px-4\",\n \"40\": \"px-4\",\n \"32\": \"px-3\",\n};\n\nconst INPUT_TYPOGRAPHY: Record<PhoneFieldSize, string> = {\n \"48\": \"typography-body-default-16px-regular autofill-body-lg\",\n \"40\": \"typography-body-default-16px-regular autofill-body-lg\",\n \"32\": \"typography-body-small-14px-regular autofill-body-md\",\n};\n\nconst PREFIX_TYPOGRAPHY: Record<PhoneFieldSize, string> = {\n \"48\": \"typography-body-default-16px-regular\",\n \"40\": \"typography-body-default-16px-regular\",\n \"32\": \"typography-body-small-14px-regular\",\n};\n\nfunction getContainerClassName(size: PhoneFieldSize, error: boolean, disabled?: boolean) {\n return cn(\n \"relative flex items-center gap-2 overflow-hidden rounded-sm border bg-inputs-inputs-primary after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] has-focus-visible:outline-none has-focus-visible:after:shadow-focus-ring motion-safe:transition-colors\",\n CONTAINER_PADDING_X[size],\n CONTAINER_HEIGHT[size],\n error ? \"border-error-content\" : \"border-border-primary\",\n !disabled && !error && \"hover:border-neutral-alphas-400\",\n disabled && \"opacity-50\",\n );\n}\n\nfunction warnMissingAccessibleName(label?: string, ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!label && !ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"PhoneField: no accessible name provided. Pass a `label`, `aria-label`, or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\nfunction PhoneFieldHelperText({\n id,\n error,\n children,\n}: {\n id: string;\n error: boolean;\n children: React.ReactNode;\n}) {\n return (\n <p\n id={id}\n className={cn(\n \"typography-description-12px-regular pt-2\",\n error ? \"text-error-content\" : \"text-content-tertiary\",\n )}\n >\n {children}\n </p>\n );\n}\n\n/**\n * A phone number input with a country selector (flag + chevron), a fixed dial\n * code prefix, and a `tel` input. The country picker itself is owned by the\n * consumer: pass the selected country's `flag` and `dialCode`, and open your\n * picker from `onCountrySelect`.\n *\n * Provide at least one of `label`, `aria-label`, or `aria-labelledby` for\n * accessibility — a console warning is emitted in development if none are set.\n *\n * @example\n * ```tsx\n * <PhoneField\n * label=\"Phone number\"\n * flag={<img src={italyFlag} alt=\"\" />}\n * dialCode=\"+39\"\n * onCountrySelect={openCountryPicker}\n * value={number}\n * onChange={(e) => setNumber(e.target.value)}\n * />\n * ```\n */\nexport const PhoneField = React.forwardRef<HTMLInputElement, PhoneFieldProps>(\n (\n {\n label,\n helperText,\n size = \"48\",\n error = false,\n errorMessage,\n dialCode,\n flag,\n onCountrySelect,\n countryButtonLabel = \"Select country\",\n className,\n id,\n disabled,\n fullWidth = false,\n type = \"tel\",\n ...props\n },\n ref,\n ) => {\n const generatedId = React.useId();\n const inputId = id || generatedId;\n const helperTextId = `${inputId}-helper`;\n const dialCodeId = `${inputId}-dial-code`;\n const bottomText = error && errorMessage ? errorMessage : helperText;\n\n const describedBy =\n [dialCode != null ? dialCodeId : null, bottomText ? helperTextId : null]\n .filter(Boolean)\n .join(\" \") || undefined;\n\n const innerRef = React.useRef<HTMLInputElement>(null);\n const setRefs = React.useCallback(\n (node: HTMLInputElement | null) => {\n innerRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) (ref as React.RefObject<HTMLInputElement | null>).current = node;\n },\n [ref],\n );\n\n const handleContainerMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {\n if (disabled) return;\n const target = event.target as HTMLElement;\n if (target === innerRef.current) return;\n if (target.closest(\"[data-pf-interactive]\")) return;\n event.preventDefault();\n innerRef.current?.focus();\n };\n\n warnMissingAccessibleName(label, props[\"aria-label\"], props[\"aria-labelledby\"]);\n\n return (\n <div\n className={cn(\"flex flex-col\", fullWidth && \"w-full\", className)}\n data-disabled={disabled ? \"\" : undefined}\n data-error={error ? \"\" : undefined}\n >\n {label && (\n <label\n htmlFor={inputId}\n className=\"typography-description-12px-semibold pb-2 text-content-primary\"\n >\n {label}\n </label>\n )}\n\n {/* biome-ignore lint/a11y/noStaticElementInteractions: focus bridge delegates pointer clicks on the padding to the input */}\n <div\n className={getContainerClassName(size, error, disabled)}\n onMouseDown={handleContainerMouseDown}\n >\n <button\n type=\"button\"\n data-pf-interactive=\"\"\n onClick={onCountrySelect}\n disabled={disabled}\n aria-label={countryButtonLabel}\n className=\"flex shrink-0 items-center gap-1 rounded-2xs focus-visible:shadow-focus-ring focus-visible:outline-none disabled:cursor-not-allowed\"\n >\n {flag != null && (\n <span className=\"flex size-6 shrink-0 items-center justify-center overflow-hidden rounded-full\">\n {flag}\n </span>\n )}\n <ChevronDownIcon size={16} className=\"shrink-0 text-content-secondary\" />\n </button>\n\n {dialCode != null && (\n <span\n id={dialCodeId}\n className={cn(\n \"shrink-0 select-none whitespace-nowrap text-content-primary\",\n PREFIX_TYPOGRAPHY[size],\n )}\n >\n {dialCode}\n </span>\n )}\n\n <input\n ref={setRefs}\n id={inputId}\n type={type}\n inputMode=\"tel\"\n disabled={disabled}\n aria-describedby={describedBy}\n aria-invalid={error || undefined}\n className={cn(\n \"h-full min-w-0 flex-1 bg-transparent text-content-primary no-underline placeholder:text-content-tertiary focus:outline-none disabled:cursor-not-allowed\",\n INPUT_TYPOGRAPHY[size],\n )}\n {...props}\n />\n </div>\n\n {bottomText && (\n <PhoneFieldHelperText id={helperTextId} error={error}>\n {bottomText}\n </PhoneFieldHelperText>\n )}\n </div>\n );\n },\n);\n\nPhoneField.displayName = \"PhoneField\";\n"],"names":[],"mappings":";;;;;AA+BA,MAAM,mBAAmD;AAAA,EACvD,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACR;AAEA,MAAM,sBAAsD;AAAA,EAC1D,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACR;AAEA,MAAM,mBAAmD;AAAA,EACvD,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACR;AAEA,MAAM,oBAAoD;AAAA,EACxD,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AACR;AAEA,SAAS,sBAAsB,MAAsB,OAAgB,UAAoB;AACvF,SAAO;AAAA,IACL;AAAA,IACA,oBAAoB,IAAI;AAAA,IACxB,iBAAiB,IAAI;AAAA,IACrB,QAAQ,yBAAyB;AAAA,IACjC,CAAC,YAAY,CAAC,SAAS;AAAA,IACvB,YAAY;AAAA,EAAA;AAEhB;AAEA,SAAS,0BAA0B,OAAgB,WAAoB,gBAAyB;AAC9F,MAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,QAAI,CAAC,SAAS,CAAC,aAAa,CAAC,gBAAgB;AAC3C,cAAQ;AAAA,QACN;AAAA,MAAA;AAAA,IAEJ;AAAA,EACF;AACF;AAEA,SAAS,qBAAqB;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA,QAAQ,uBAAuB;AAAA,MAAA;AAAA,MAGhC;AAAA,IAAA;AAAA,EAAA;AAGP;AAuBO,MAAM,aAAa,MAAM;AAAA,EAC9B,CACE;AAAA,IACE;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,qBAAqB;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ,OAAO;AAAA,IACP,GAAG;AAAA,EAAA,GAEL,QACG;AACH,UAAM,cAAc,MAAM,MAAA;AAC1B,UAAM,UAAU,MAAM;AACtB,UAAM,eAAe,GAAG,OAAO;AAC/B,UAAM,aAAa,GAAG,OAAO;AAC7B,UAAM,aAAa,SAAS,eAAe,eAAe;AAE1D,UAAM,cACJ,CAAC,YAAY,OAAO,aAAa,MAAM,aAAa,eAAe,IAAI,EACpE,OAAO,OAAO,EACd,KAAK,GAAG,KAAK;AAElB,UAAM,WAAW,MAAM,OAAyB,IAAI;AACpD,UAAM,UAAU,MAAM;AAAA,MACpB,CAAC,SAAkC;AACjC,iBAAS,UAAU;AACnB,YAAI,OAAO,QAAQ,WAAY,KAAI,IAAI;AAAA,iBAC9B,IAAM,KAAiD,UAAU;AAAA,MAC5E;AAAA,MACA,CAAC,GAAG;AAAA,IAAA;AAGN,UAAM,2BAA2B,CAAC,UAA4C;AAC5E,UAAI,SAAU;AACd,YAAM,SAAS,MAAM;AACrB,UAAI,WAAW,SAAS,QAAS;AACjC,UAAI,OAAO,QAAQ,uBAAuB,EAAG;AAC7C,YAAM,eAAA;AACN,eAAS,SAAS,MAAA;AAAA,IACpB;AAEA,8BAA0B,OAAO,MAAM,YAAY,GAAG,MAAM,iBAAiB,CAAC;AAE9E,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAW,GAAG,iBAAiB,aAAa,UAAU,SAAS;AAAA,QAC/D,iBAAe,WAAW,KAAK;AAAA,QAC/B,cAAY,QAAQ,KAAK;AAAA,QAExB,UAAA;AAAA,UAAA,SACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,SAAS;AAAA,cACT,WAAU;AAAA,cAET,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAKL;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW,sBAAsB,MAAM,OAAO,QAAQ;AAAA,cACtD,aAAa;AAAA,cAEb,UAAA;AAAA,gBAAA;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,MAAK;AAAA,oBACL,uBAAoB;AAAA,oBACpB,SAAS;AAAA,oBACT;AAAA,oBACA,cAAY;AAAA,oBACZ,WAAU;AAAA,oBAET,UAAA;AAAA,sBAAA,QAAQ,QACP,oBAAC,QAAA,EAAK,WAAU,iFACb,UAAA,MACH;AAAA,sBAEF,oBAAC,iBAAA,EAAgB,MAAM,IAAI,WAAU,kCAAA,CAAkC;AAAA,oBAAA;AAAA,kBAAA;AAAA,gBAAA;AAAA,gBAGxE,YAAY,QACX;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,IAAI;AAAA,oBACJ,WAAW;AAAA,sBACT;AAAA,sBACA,kBAAkB,IAAI;AAAA,oBAAA;AAAA,oBAGvB,UAAA;AAAA,kBAAA;AAAA,gBAAA;AAAA,gBAIL;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,KAAK;AAAA,oBACL,IAAI;AAAA,oBACJ;AAAA,oBACA,WAAU;AAAA,oBACV;AAAA,oBACA,oBAAkB;AAAA,oBAClB,gBAAc,SAAS;AAAA,oBACvB,WAAW;AAAA,sBACT;AAAA,sBACA,iBAAiB,IAAI;AAAA,oBAAA;AAAA,oBAEtB,GAAG;AAAA,kBAAA;AAAA,gBAAA;AAAA,cACN;AAAA,YAAA;AAAA,UAAA;AAAA,UAGD,cACC,oBAAC,sBAAA,EAAqB,IAAI,cAAc,OACrC,UAAA,WAAA,CACH;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAIR;AACF;AAEA,WAAW,cAAc;"}
|
|
@@ -32,7 +32,7 @@ const CLEAR_BUTTON_RIGHT = {
|
|
|
32
32
|
};
|
|
33
33
|
function getContainerClassName(size, error, disabled) {
|
|
34
34
|
return cn(
|
|
35
|
-
"relative rounded-sm border bg-inputs-inputs-primary
|
|
35
|
+
"relative rounded-sm border bg-inputs-inputs-primary after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] has-focus-visible:outline-none has-focus-visible:after:shadow-focus-ring motion-safe:transition-colors",
|
|
36
36
|
error ? "border-error-content" : "border-border-primary",
|
|
37
37
|
!disabled && !error && "hover:border-neutral-alphas-400",
|
|
38
38
|
CONTAINER_MIN_HEIGHT[size],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextArea.mjs","sources":["../../../src/components/TextArea/TextArea.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { CheckOutlineIcon } from \"../Icons/CheckOutlineIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\n\n/** Text area height in pixels. */\nexport type TextAreaSize = \"48\" | \"40\" | \"32\";\n\nexport interface TextAreaProps\n extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, \"size\"> {\n /** Label text displayed above the textarea. Also used as the accessible name. */\n label?: string;\n /** Helper text displayed below the textarea. Replaced by `errorMessage` when `error` is `true`. */\n helperText?: string;\n /** Minimum height of the text area in pixels. @default \"48\" */\n size?: TextAreaSize;\n /** Whether the text area is in an error state. @default false */\n error?: boolean;\n /** Error message displayed below the textarea. Shown instead of `helperText` when `error` is `true`. */\n errorMessage?: string;\n /** Whether the text area is validated. @default false */\n validated?: boolean;\n /** Whether the text area stretches to fill its container width. @default false */\n fullWidth?: boolean;\n /** Whether to show a clear button when text is present. @default false */\n showClearButton?: boolean;\n /** Callback fired when the clear button is clicked. Note: `onChange` is also called with an empty value when clearing. */\n onClear?: () => void;\n /** Minimum number of rows (lines) for the textarea. */\n minRows?: number;\n /** Maximum number of rows (lines) for the textarea. */\n maxRows?: number;\n /** Whether the textarea can be resized by the user. @default true */\n resizable?: boolean;\n}\n\nconst CONTAINER_MIN_HEIGHT: Record<TextAreaSize, string> = {\n \"48\": \"min-h-12\",\n \"40\": \"min-h-10\",\n \"32\": \"min-h-8\",\n};\n\nconst TEXTAREA_SIZE_CLASSES: Record<TextAreaSize, string> = {\n \"48\": \"py-3 typography-body-default-16px-regular autofill-body-lg\",\n \"40\": \"py-2 typography-body-default-16px-regular autofill-body-lg\",\n \"32\": \"py-2 typography-body-small-14px-regular autofill-body-md\",\n};\n\nconst PADDING_HORIZONTAL: Record<TextAreaSize, string> = {\n \"48\": \"px-4\",\n \"40\": \"px-4\",\n \"32\": \"px-3\",\n};\n\nconst PADDING_RIGHT_WITH_CLEAR: Record<TextAreaSize, string> = {\n \"48\": \"pr-11\",\n \"40\": \"pr-11\",\n \"32\": \"pr-10\",\n};\n\nconst CLEAR_BUTTON_RIGHT: Record<TextAreaSize, string> = {\n \"48\": \"right-4 top-3\",\n \"40\": \"right-4 top-2\",\n \"32\": \"right-3 top-2\",\n};\n\nfunction getContainerClassName(size: TextAreaSize, error: boolean, disabled?: boolean) {\n return cn(\n \"relative rounded-sm border bg-inputs-inputs-primary has-focus-visible:shadow-focus-ring has-focus-visible:outline-none motion-safe:transition-colors\",\n error ? \"border-error-content\" : \"border-border-primary\",\n !disabled && !error && \"hover:border-neutral-alphas-400\",\n CONTAINER_MIN_HEIGHT[size],\n disabled && \"opacity-50\",\n );\n}\n\nfunction getTextareaClassName(\n size: TextAreaSize,\n hasClearButton: boolean,\n hasMinRows: boolean,\n resizable: boolean,\n) {\n return cn(\n \"h-full w-full bg-transparent text-content-primary no-underline placeholder:text-content-tertiary focus:outline-none disabled:cursor-not-allowed\",\n resizable ? \"resize-y\" : \"resize-none\",\n !hasMinRows && \"min-h-[80px]\",\n TEXTAREA_SIZE_CLASSES[size],\n PADDING_HORIZONTAL[size],\n hasClearButton ? PADDING_RIGHT_WITH_CLEAR[size] : \"\",\n );\n}\n\nfunction TextAreaHelperText({\n id,\n error,\n children,\n}: {\n id: string;\n error: boolean;\n children: React.ReactNode;\n}) {\n return (\n <p\n id={id}\n className={cn(\n \"typography-description-12px-regular pt-2\",\n error ? \"text-error-content\" : \"text-content-tertiary\",\n )}\n >\n {children}\n </p>\n );\n}\n\nfunction warnMissingAccessibleName(label?: string, ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!label && !ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"TextArea: no accessible name provided. Pass a `label`, `aria-label`, or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\nfunction calculateMaxHeight(size: TextAreaSize, maxRows?: number): string | undefined {\n if (!maxRows) return undefined;\n\n // Line height is 24px for body-1 (sizes 48 and 40) and 20px for body-2 (size 32)\n const lineHeight = size === \"32\" ? 20 : 24;\n // py-2 = 8px, py-3 = 12px\n const verticalPadding = size === \"32\" ? 8 : size === \"40\" ? 8 : 12;\n\n return `${lineHeight * maxRows + verticalPadding * 2}px`;\n}\n\nfunction useTextAreaValue(\n value: React.TextareaHTMLAttributes<HTMLTextAreaElement>[\"value\"],\n defaultValue: React.TextareaHTMLAttributes<HTMLTextAreaElement>[\"defaultValue\"],\n showClearButton: boolean,\n onChange?: React.ChangeEventHandler<HTMLTextAreaElement>,\n onClear?: () => void,\n textareaRef?: React.RefObject<HTMLTextAreaElement | null>,\n) {\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? \"\");\n const resolvedValue = value !== undefined ? value : internalValue;\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n setInternalValue(e.target.value);\n onChange?.(e);\n };\n\n const handleClear = () => {\n setInternalValue(\"\");\n\n if (onChange && textareaRef?.current) {\n const syntheticEvent = {\n target: { ...textareaRef.current, value: \"\" },\n currentTarget: { ...textareaRef.current, value: \"\" },\n } as React.ChangeEvent<HTMLTextAreaElement>;\n onChange(syntheticEvent);\n }\n\n onClear?.();\n };\n\n return {\n resolvedValue,\n displayValue: showClearButton ? resolvedValue : value,\n resolvedDefaultValue: showClearButton ? undefined : defaultValue,\n handleChange,\n handleClear,\n };\n}\n\n/**\n * A multi-line text input with optional label, helper/error text, and clear button.\n *\n * Provide at least one of `label`, `aria-label`, or `aria-labelledby` for\n * accessibility — a console warning is emitted in development if none are set.\n *\n * @example\n * ```tsx\n * <TextArea\n * label=\"Description\"\n * placeholder=\"Enter your description...\"\n * showClearButton\n * error={!!descError}\n * errorMessage={descError}\n * />\n * ```\n */\nexport const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(\n (\n {\n label,\n helperText,\n size = \"48\",\n error = false,\n errorMessage,\n validated = false,\n className,\n id,\n disabled,\n fullWidth = false,\n showClearButton = false,\n onClear,\n value,\n defaultValue,\n onChange,\n minRows,\n maxRows,\n resizable = false,\n ...props\n },\n ref,\n ) => {\n const generatedId = React.useId();\n const inputId = id || generatedId;\n const helperTextId = `${inputId}-helper`;\n const bottomText = error && errorMessage ? errorMessage : helperText;\n const maxHeight = calculateMaxHeight(size, maxRows);\n\n const internalRef = React.useRef<HTMLTextAreaElement>(null);\n\n const { resolvedValue, displayValue, resolvedDefaultValue, handleChange, handleClear } =\n useTextAreaValue(value, defaultValue, showClearButton, onChange, onClear, internalRef);\n\n const mergedRef = (node: HTMLTextAreaElement | null) => {\n internalRef.current = node;\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;\n }\n };\n\n const showClear = showClearButton && resolvedValue !== \"\" && !disabled;\n const showValidated = validated && !showClear;\n const ariaDescribedBy = bottomText ? helperTextId : undefined;\n const textareaStyle = maxHeight ? { maxHeight } : undefined;\n\n warnMissingAccessibleName(label, props[\"aria-label\"], props[\"aria-labelledby\"]);\n\n return (\n <div\n className={cn(\"flex flex-col\", fullWidth && \"w-full\", className)}\n data-disabled={disabled ? \"\" : undefined}\n data-error={error ? \"\" : undefined}\n >\n {label && (\n <label\n htmlFor={inputId}\n className=\"typography-description-12px-semibold pb-2 text-content-primary\"\n >\n {label}\n </label>\n )}\n\n <div className={getContainerClassName(size, error, disabled)}>\n <textarea\n ref={mergedRef}\n id={inputId}\n disabled={disabled}\n aria-describedby={ariaDescribedBy}\n aria-invalid={error || undefined}\n className={getTextareaClassName(size, showClear, !!minRows, resizable)}\n value={displayValue}\n defaultValue={resolvedDefaultValue}\n onChange={handleChange}\n rows={minRows}\n style={textareaStyle}\n {...props}\n />\n\n {showClear && (\n <IconButton\n variant=\"tertiary\"\n size=\"24\"\n icon={<CloseIcon />}\n aria-label=\"Clear text\"\n onClick={handleClear}\n className={cn(\n \"absolute flex size-5 items-center justify-center self-end\",\n CLEAR_BUTTON_RIGHT[size],\n )}\n />\n )}\n {showValidated && (\n <div\n className={cn(\n \"pointer-events-none absolute flex size-5 items-center justify-center\",\n CLEAR_BUTTON_RIGHT[size],\n )}\n >\n <CheckOutlineIcon className=\"text-success-content\" />\n </div>\n )}\n </div>\n\n {bottomText && (\n <TextAreaHelperText id={helperTextId} error={error}>\n {bottomText}\n </TextAreaHelperText>\n )}\n </div>\n );\n },\n);\n\nTextArea.displayName = \"TextArea\";\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TextArea.mjs","sources":["../../../src/components/TextArea/TextArea.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { IconButton } from \"../IconButton/IconButton\";\nimport { CheckOutlineIcon } from \"../Icons/CheckOutlineIcon\";\nimport { CloseIcon } from \"../Icons/CloseIcon\";\n\n/** Text area height in pixels. */\nexport type TextAreaSize = \"48\" | \"40\" | \"32\";\n\nexport interface TextAreaProps\n extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, \"size\"> {\n /** Label text displayed above the textarea. Also used as the accessible name. */\n label?: string;\n /** Helper text displayed below the textarea. Replaced by `errorMessage` when `error` is `true`. */\n helperText?: string;\n /** Minimum height of the text area in pixels. @default \"48\" */\n size?: TextAreaSize;\n /** Whether the text area is in an error state. @default false */\n error?: boolean;\n /** Error message displayed below the textarea. Shown instead of `helperText` when `error` is `true`. */\n errorMessage?: string;\n /** Whether the text area is validated. @default false */\n validated?: boolean;\n /** Whether the text area stretches to fill its container width. @default false */\n fullWidth?: boolean;\n /** Whether to show a clear button when text is present. @default false */\n showClearButton?: boolean;\n /** Callback fired when the clear button is clicked. Note: `onChange` is also called with an empty value when clearing. */\n onClear?: () => void;\n /** Minimum number of rows (lines) for the textarea. */\n minRows?: number;\n /** Maximum number of rows (lines) for the textarea. */\n maxRows?: number;\n /** Whether the textarea can be resized by the user. @default true */\n resizable?: boolean;\n}\n\nconst CONTAINER_MIN_HEIGHT: Record<TextAreaSize, string> = {\n \"48\": \"min-h-12\",\n \"40\": \"min-h-10\",\n \"32\": \"min-h-8\",\n};\n\nconst TEXTAREA_SIZE_CLASSES: Record<TextAreaSize, string> = {\n \"48\": \"py-3 typography-body-default-16px-regular autofill-body-lg\",\n \"40\": \"py-2 typography-body-default-16px-regular autofill-body-lg\",\n \"32\": \"py-2 typography-body-small-14px-regular autofill-body-md\",\n};\n\nconst PADDING_HORIZONTAL: Record<TextAreaSize, string> = {\n \"48\": \"px-4\",\n \"40\": \"px-4\",\n \"32\": \"px-3\",\n};\n\nconst PADDING_RIGHT_WITH_CLEAR: Record<TextAreaSize, string> = {\n \"48\": \"pr-11\",\n \"40\": \"pr-11\",\n \"32\": \"pr-10\",\n};\n\nconst CLEAR_BUTTON_RIGHT: Record<TextAreaSize, string> = {\n \"48\": \"right-4 top-3\",\n \"40\": \"right-4 top-2\",\n \"32\": \"right-3 top-2\",\n};\n\nfunction getContainerClassName(size: TextAreaSize, error: boolean, disabled?: boolean) {\n return cn(\n \"relative rounded-sm border bg-inputs-inputs-primary after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] has-focus-visible:outline-none has-focus-visible:after:shadow-focus-ring motion-safe:transition-colors\",\n error ? \"border-error-content\" : \"border-border-primary\",\n !disabled && !error && \"hover:border-neutral-alphas-400\",\n CONTAINER_MIN_HEIGHT[size],\n disabled && \"opacity-50\",\n );\n}\n\nfunction getTextareaClassName(\n size: TextAreaSize,\n hasClearButton: boolean,\n hasMinRows: boolean,\n resizable: boolean,\n) {\n return cn(\n \"h-full w-full bg-transparent text-content-primary no-underline placeholder:text-content-tertiary focus:outline-none disabled:cursor-not-allowed\",\n resizable ? \"resize-y\" : \"resize-none\",\n !hasMinRows && \"min-h-[80px]\",\n TEXTAREA_SIZE_CLASSES[size],\n PADDING_HORIZONTAL[size],\n hasClearButton ? PADDING_RIGHT_WITH_CLEAR[size] : \"\",\n );\n}\n\nfunction TextAreaHelperText({\n id,\n error,\n children,\n}: {\n id: string;\n error: boolean;\n children: React.ReactNode;\n}) {\n return (\n <p\n id={id}\n className={cn(\n \"typography-description-12px-regular pt-2\",\n error ? \"text-error-content\" : \"text-content-tertiary\",\n )}\n >\n {children}\n </p>\n );\n}\n\nfunction warnMissingAccessibleName(label?: string, ariaLabel?: string, ariaLabelledBy?: string) {\n if (process.env.NODE_ENV !== \"production\") {\n if (!label && !ariaLabel && !ariaLabelledBy) {\n console.warn(\n \"TextArea: no accessible name provided. Pass a `label`, `aria-label`, or `aria-labelledby` prop.\",\n );\n }\n }\n}\n\nfunction calculateMaxHeight(size: TextAreaSize, maxRows?: number): string | undefined {\n if (!maxRows) return undefined;\n\n // Line height is 24px for body-1 (sizes 48 and 40) and 20px for body-2 (size 32)\n const lineHeight = size === \"32\" ? 20 : 24;\n // py-2 = 8px, py-3 = 12px\n const verticalPadding = size === \"32\" ? 8 : size === \"40\" ? 8 : 12;\n\n return `${lineHeight * maxRows + verticalPadding * 2}px`;\n}\n\nfunction useTextAreaValue(\n value: React.TextareaHTMLAttributes<HTMLTextAreaElement>[\"value\"],\n defaultValue: React.TextareaHTMLAttributes<HTMLTextAreaElement>[\"defaultValue\"],\n showClearButton: boolean,\n onChange?: React.ChangeEventHandler<HTMLTextAreaElement>,\n onClear?: () => void,\n textareaRef?: React.RefObject<HTMLTextAreaElement | null>,\n) {\n const [internalValue, setInternalValue] = React.useState(defaultValue ?? \"\");\n const resolvedValue = value !== undefined ? value : internalValue;\n\n const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n setInternalValue(e.target.value);\n onChange?.(e);\n };\n\n const handleClear = () => {\n setInternalValue(\"\");\n\n if (onChange && textareaRef?.current) {\n const syntheticEvent = {\n target: { ...textareaRef.current, value: \"\" },\n currentTarget: { ...textareaRef.current, value: \"\" },\n } as React.ChangeEvent<HTMLTextAreaElement>;\n onChange(syntheticEvent);\n }\n\n onClear?.();\n };\n\n return {\n resolvedValue,\n displayValue: showClearButton ? resolvedValue : value,\n resolvedDefaultValue: showClearButton ? undefined : defaultValue,\n handleChange,\n handleClear,\n };\n}\n\n/**\n * A multi-line text input with optional label, helper/error text, and clear button.\n *\n * Provide at least one of `label`, `aria-label`, or `aria-labelledby` for\n * accessibility — a console warning is emitted in development if none are set.\n *\n * @example\n * ```tsx\n * <TextArea\n * label=\"Description\"\n * placeholder=\"Enter your description...\"\n * showClearButton\n * error={!!descError}\n * errorMessage={descError}\n * />\n * ```\n */\nexport const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(\n (\n {\n label,\n helperText,\n size = \"48\",\n error = false,\n errorMessage,\n validated = false,\n className,\n id,\n disabled,\n fullWidth = false,\n showClearButton = false,\n onClear,\n value,\n defaultValue,\n onChange,\n minRows,\n maxRows,\n resizable = false,\n ...props\n },\n ref,\n ) => {\n const generatedId = React.useId();\n const inputId = id || generatedId;\n const helperTextId = `${inputId}-helper`;\n const bottomText = error && errorMessage ? errorMessage : helperText;\n const maxHeight = calculateMaxHeight(size, maxRows);\n\n const internalRef = React.useRef<HTMLTextAreaElement>(null);\n\n const { resolvedValue, displayValue, resolvedDefaultValue, handleChange, handleClear } =\n useTextAreaValue(value, defaultValue, showClearButton, onChange, onClear, internalRef);\n\n const mergedRef = (node: HTMLTextAreaElement | null) => {\n internalRef.current = node;\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref) {\n (ref as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;\n }\n };\n\n const showClear = showClearButton && resolvedValue !== \"\" && !disabled;\n const showValidated = validated && !showClear;\n const ariaDescribedBy = bottomText ? helperTextId : undefined;\n const textareaStyle = maxHeight ? { maxHeight } : undefined;\n\n warnMissingAccessibleName(label, props[\"aria-label\"], props[\"aria-labelledby\"]);\n\n return (\n <div\n className={cn(\"flex flex-col\", fullWidth && \"w-full\", className)}\n data-disabled={disabled ? \"\" : undefined}\n data-error={error ? \"\" : undefined}\n >\n {label && (\n <label\n htmlFor={inputId}\n className=\"typography-description-12px-semibold pb-2 text-content-primary\"\n >\n {label}\n </label>\n )}\n\n <div className={getContainerClassName(size, error, disabled)}>\n <textarea\n ref={mergedRef}\n id={inputId}\n disabled={disabled}\n aria-describedby={ariaDescribedBy}\n aria-invalid={error || undefined}\n className={getTextareaClassName(size, showClear, !!minRows, resizable)}\n value={displayValue}\n defaultValue={resolvedDefaultValue}\n onChange={handleChange}\n rows={minRows}\n style={textareaStyle}\n {...props}\n />\n\n {showClear && (\n <IconButton\n variant=\"tertiary\"\n size=\"24\"\n icon={<CloseIcon />}\n aria-label=\"Clear text\"\n onClick={handleClear}\n className={cn(\n \"absolute flex size-5 items-center justify-center self-end\",\n CLEAR_BUTTON_RIGHT[size],\n )}\n />\n )}\n {showValidated && (\n <div\n className={cn(\n \"pointer-events-none absolute flex size-5 items-center justify-center\",\n CLEAR_BUTTON_RIGHT[size],\n )}\n >\n <CheckOutlineIcon className=\"text-success-content\" />\n </div>\n )}\n </div>\n\n {bottomText && (\n <TextAreaHelperText id={helperTextId} error={error}>\n {bottomText}\n </TextAreaHelperText>\n )}\n </div>\n );\n },\n);\n\nTextArea.displayName = \"TextArea\";\n"],"names":[],"mappings":"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"}
|
|
@@ -36,7 +36,7 @@ const SIDE_LABEL_TYPOGRAPHY = {
|
|
|
36
36
|
};
|
|
37
37
|
function getContainerClassName(size, error, disabled, hasAction) {
|
|
38
38
|
return cn(
|
|
39
|
-
"relative flex items-center overflow-hidden rounded-sm border bg-inputs-inputs-primary
|
|
39
|
+
"relative flex items-center overflow-hidden rounded-sm border bg-inputs-inputs-primary after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] has-focus-visible:outline-none has-focus-visible:after:shadow-focus-ring motion-safe:transition-colors",
|
|
40
40
|
hasAction ? CONTAINER_PADDING_X_WITH_ACTION[size] : CONTAINER_PADDING_X[size],
|
|
41
41
|
CONTAINER_GAP[size],
|
|
42
42
|
error ? "border-error-content" : "border-border-primary",
|