@stackone/malachite 2.31.1 → 2.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMPONENTS.md +1 -1
- package/dist/components/FieldMessage.js.map +1 -1
- package/dist/components/Input/Input.js +65 -58
- package/dist/components/Input/Input.js.map +1 -1
- package/dist/components/SelectableCard.js +217 -194
- package/dist/components/SelectableCard.js.map +1 -1
- package/dist/components/SelectionBar.js +1 -1
- package/dist/components/SelectionBar.js.map +1 -1
- package/dist/components/SidePanel.js +60 -36
- package/dist/components/SidePanel.js.map +1 -1
- package/dist/components/TextArea/TextArea.js +1 -1
- package/dist/components/TextArea/TextArea.js.map +1 -1
- package/dist/components/Tooltip.js +57 -56
- package/dist/components/Tooltip.js.map +1 -1
- package/dist/components/Typography/CodeText.js +45 -22
- package/dist/components/Typography/CodeText.js.map +1 -1
- package/dist/components/Typography/EllipsisText.js +14 -12
- package/dist/components/Typography/EllipsisText.js.map +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/tailwind.css.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextArea.js","sources":["../../../src/components/TextArea/TextArea.tsx"],"sourcesContent":["import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';\nimport type { Control, FieldPath, FieldValues } from 'react-hook-form';\nimport BaseFormComponent, { type RequiredIndicator, useFormFieldIds } from '../BaseFormComponent';\nimport { Button } from '../Button';\nimport CopyIcon from '../CustomIcons/CopyIcon';\nimport InfoIcon from '../CustomIcons/InfoIcon';\nimport FieldMessage from '../FieldMessage';\nimport { paintedFieldValue } from '../devWarning';\nimport type { ButtonDefinition, TextAreaSize } from './types';\n\n/**\n * `assistant` is the chrome for a field sitting in an AI context (e.g. `Chat.Composer`'s\n * policy-assistant tone): the field's own focus border recolours to the assistant violet\n * instead of the default accent — same indicator, different colour, not an extra one. Same\n * naming precedent as `PillButton`'s own `tone`.\n */\ntype TextAreaTone = 'default' | 'assistant';\n\ninterface TextAreaProps {\n label?: string;\n hint?: string;\n tooltip?: string;\n placeholder?: string;\n icon?: React.ReactNode;\n readOnly?: boolean;\n name: FieldPath<FieldValues> | string;\n description?: string;\n size?: TextAreaSize;\n required?: boolean;\n requiredIndicator?: RequiredIndicator;\n error?: boolean;\n loading?: boolean;\n defaultValue?: string;\n disabled?: boolean;\n prefixValue?: string;\n prefixSecondaryValue?: string;\n ellipsis?: boolean;\n formControl?: Control<FieldValues, unknown>;\n buttons?: ButtonDefinition[];\n onCopyClick?: () => void;\n onChange?: (value: string) => void;\n value?: string;\n onBlur?: () => void;\n /** Key handling the control cannot own, e.g. Enter-to-submit in a chat composer. */\n onKeyDown?: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;\n /**\n * A control that belongs inside the field, pinned bottom-right — a chat composer's send.\n * The field reserves room for it, so text cannot run underneath.\n */\n inFieldAction?: React.ReactNode;\n /** See `TextAreaTone`. Defaults to `'default'`, so no existing field changes. */\n tone?: TextAreaTone;\n helperText?: string;\n rows?: number;\n /**\n * Grows the field from `rows` up to this many lines as content is typed, then scrolls\n * internally rather than growing further. Omitted keeps the field at a fixed `rows`\n * height, the pre-existing behaviour.\n */\n maxRows?: number;\n maxLength?: number;\n resize?: 'none' | 'vertical' | 'horizontal' | 'both';\n /**\n * When false, lines are not soft-wrapped — long content scrolls horizontally\n * instead of wrapping (native `wrap=\"off\"`). Useful for one-value-per-line\n * content like URLs. Defaults to true.\n */\n wrap?: boolean;\n id?: string;\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n}\n\nconst getSizeClasses = (size: TextAreaSize): string => {\n switch (size) {\n case 'small':\n return 'text-sm';\n case 'medium':\n return 'text-[length:var(--malachite-font-size-15,15px)] leading-[1.4]';\n case 'large':\n return 'text-base';\n default:\n return 'text-[length:var(--malachite-font-size-15,15px)] leading-[1.4]';\n }\n};\n\n/**\n * A rejection always wins the border colour, regardless of `tone`: a field the caller has\n * flagged invalid stays destructive-red on focus even inside an assistant surface.\n *\n * Each branch spells its two classes out in full rather than interpolating a shared colour\n * variable into them. Tailwind extracts candidates by scanning source text, so a class built\n * at runtime is one Tailwind never sees — and the CSS is simply absent, with no build error.\n * The assistant pair was surviving only because the same literal appears in a spec's\n * assertion, which the content glob also scans: rewording that assertion would have silently\n * dropped the violet focus border.\n */\nconst focusBorderClasses = (isError: boolean, tone: TextAreaTone): string => {\n if (isError) {\n return 'focus-within:border-[var(--malachite-destructive-border-hover)] hover:border-[var(--malachite-destructive-border-hover)]';\n }\n\n if (tone === 'assistant') {\n return 'focus-within:border-[var(--malachite-assistant-foreground)] hover:border-[var(--malachite-assistant-foreground)]';\n }\n\n return 'focus-within:border-[var(--malachite-control-border-hover)] hover:border-[var(--malachite-control-border-hover)]';\n};\n\n// Shared by the static min-height and the auto-grow ceiling, so a field's grown height\n// lines up with the same row metrics its resting height already used.\nconst rowHeightPx = (size: TextAreaSize, rows: number): number => {\n const baseHeight = {\n small: 20,\n medium: 22,\n large: 24,\n }[size];\n\n // Every size renders `py-[12px]`, so the vertical padding is 24 regardless of size.\n const padding = 24;\n\n return baseHeight * rows + padding;\n};\n\nconst getMinHeight = (size: TextAreaSize, rows: number): string => `${rowHeightPx(size, rows)}px`;\n\nconst TextArea: React.FC<TextAreaProps> = ({\n label,\n hint,\n tooltip,\n placeholder,\n icon,\n name,\n required,\n requiredIndicator,\n error,\n description,\n loading = false,\n readOnly = false,\n size = 'medium',\n defaultValue,\n disabled,\n prefixValue,\n prefixSecondaryValue,\n // ellipsis = false,\n formControl,\n buttons,\n onCopyClick,\n onChange,\n onKeyDown,\n inFieldAction,\n tone = 'default',\n value,\n onBlur,\n helperText,\n rows = 4,\n maxRows,\n maxLength,\n resize = 'vertical',\n wrap = true,\n id,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n}) => {\n const { controlId, labelId, descriptionId, hintId, helperTextId } = useFormFieldIds({\n id,\n label,\n description,\n hint,\n helperText: true,\n });\n const [internalValue, setInternalValue] = useState(defaultValue || '');\n const textareaRef = useRef<HTMLTextAreaElement | null>(null);\n\n // Reset-then-measure rather than growing incrementally: `scrollHeight` on a field already\n // taller than its content stays stuck at that height, so the reset is what lets a paste\n // followed by a delete shrink the field back down, not just grow it. Mutates the DOM\n // directly instead of round-tripping through React state, so typing a character never\n // re-renders this component just to change its own height.\n const resizeToFit = useCallback(() => {\n const el = textareaRef.current;\n if (!el || maxRows === undefined) {\n return;\n }\n\n const maxHeight = rowHeightPx(size, maxRows);\n el.style.height = 'auto';\n const nextHeight = Math.min(el.scrollHeight, maxHeight);\n el.style.height = `${nextHeight}px`;\n el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden';\n }, [maxRows, size]);\n\n // Catches every change `onChange` on the element itself would miss: the initial mount,\n // and a value the parent sets from outside (e.g. clearing the composer after send).\n // biome-ignore lint/correctness/useExhaustiveDependencies: `value` is not read in the body, but a controlled `value` changing from outside is exactly the case `onChange` cannot see, so the effect has to re-run on it anyway.\n useLayoutEffect(() => {\n resizeToFit();\n }, [resizeToFit, value]);\n\n const isControlled = value !== undefined;\n const paintedValue = (formValue: unknown): string =>\n paintedFieldValue({\n component: 'TextArea',\n name,\n value,\n formValue,\n defaultValue,\n bound: !!formControl,\n uncontrolledValue: internalValue,\n });\n\n const handleChange =\n (reportToForm?: (value: unknown) => void) =>\n (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n const newValue = e.target.value;\n if (!isControlled) {\n setInternalValue(newValue);\n }\n // The browser has already applied the keystroke to the element by the time this\n // fires, so `scrollHeight` already reflects it — no need to wait for a re-render.\n resizeToFit();\n onChange?.(newValue);\n reportToForm?.(newValue);\n };\n\n const hasPrefix = icon || prefixValue || prefixSecondaryValue;\n\n const errorMessageId = `${helperTextId}-error`;\n\n const renderTextArea = (\n isError: boolean,\n resolvedHelperText?: string,\n onFieldChange?: (value: unknown) => void,\n onFieldBlur?: () => void,\n errorMessage?: string,\n formValue?: unknown,\n ) => {\n const textAreaValue = paintedValue(formValue);\n // The icon is the only signal a consumer-forced `error` has; beside a message it is\n // duplication, and it crowds the suffix next to a reveal or copy control.\n const showErrorIcon = isError && !errorMessage;\n const hasSuffix =\n showErrorIcon || (onCopyClick && disabled && textAreaValue) || buttons?.length;\n const describedBy =\n [\n descriptionId,\n hintId,\n resolvedHelperText ? helperTextId : undefined,\n errorMessage ? errorMessageId : undefined,\n ]\n .filter(Boolean)\n .join(' ') || undefined;\n const wrapperClasses = [\n 'relative',\n 'flex',\n 'w-full',\n 'rounded-xl',\n 'border border-solid',\n disabled\n ? 'bg-[var(--malachite-card-dimmed-background)]'\n : 'bg-[var(--malachite-card-background)]',\n 'transition-colors duration-200 ease-in-out',\n isError ? 'shadow-error' : 'shadow-default',\n isError\n ? 'border-[var(--malachite-destructive-border)]'\n : 'border-[var(--malachite-card-border)]',\n // One border-hover colour, not two overlapping utilities for the same states: a\n // duplicate pair used to sit above this one, fixed at the default accent — since\n // it targeted the identical `focus-within`/`hover` selectors, a `tone` swap here\n // would otherwise leave a second, conflicting colour in the class list.\n //\n // No separate ring on top of it: the recoloured border IS this field's focus\n // indicator, the same as the default tone's. Adding a ring here drew a second,\n // concentric outline around the first on `assistant` — the field already has one,\n // it only needed to change colour.\n disabled ? '' : focusBorderClasses(isError, tone),\n ]\n .filter(Boolean)\n .join(' ');\n\n const textAreaClasses = [\n 'flex-1',\n 'bg-transparent',\n 'border-none',\n 'outline-none',\n 'text-[var(--malachite-text-primary)]',\n 'placeholder:text-[var(--malachite-text-placeholder)]',\n 'disabled:text-[var(--malachite-text-dimmed)]',\n 'disabled:cursor-not-allowed',\n getSizeClasses(size),\n // Emitted as one side each when an action sits in the field: `px` and `pr` are\n // the same specificity, so which wins would rest on Tailwind's output order.\n inFieldAction ? 'pl-[16px] pr-[52px]' : 'px-[16px]',\n 'py-[12px]',\n resize === 'none'\n ? 'resize-none'\n : resize === 'vertical'\n ? 'resize-y'\n : resize === 'horizontal'\n ? 'resize-x'\n : 'resize',\n wrap ? '' : 'whitespace-pre overflow-x-auto min-w-0',\n ]\n .filter(Boolean)\n .join(' ');\n\n const textAreaElement = (\n <textarea\n ref={textareaRef}\n id={controlId}\n placeholder={placeholder}\n value={textAreaValue}\n onChange={handleChange(onFieldChange)}\n onBlur={() => {\n onBlur?.();\n onFieldBlur?.();\n }}\n onKeyDown={onKeyDown}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n aria-invalid={isError || undefined}\n aria-required={required || undefined}\n aria-describedby={describedBy}\n disabled={disabled || !!prefixValue || !!prefixSecondaryValue || readOnly}\n readOnly={readOnly}\n wrap={wrap ? undefined : 'off'}\n rows={rows}\n maxLength={maxLength}\n style={{ minHeight: getMinHeight(size, rows) }}\n className={textAreaClasses}\n name={name}\n />\n );\n\n return (\n <div className={wrapperClasses}>\n {hasPrefix && (\n <div className=\"flex pt-3 pl-3 pr-1 flex-shrink-0 pointer-events-none\">\n <CompositeIcon\n prefixValue={prefixValue}\n prefixSecondaryValue={prefixSecondaryValue}\n icon={icon}\n />\n </div>\n )}\n\n {textAreaElement}\n\n {inFieldAction && (\n <div className=\"absolute bottom-2 right-2 flex items-center\">\n {inFieldAction}\n </div>\n )}\n\n {hasSuffix && (\n <div className=\"flex pt-3 pl-1 pr-3 flex-shrink-0\">\n <Suffix\n error={showErrorIcon}\n disabled={disabled}\n defaultValue={textAreaValue}\n onCopyClick={onCopyClick}\n />\n <TextAreaButtons buttons={buttons} />\n </div>\n )}\n </div>\n );\n };\n\n const renderHelperText = () =>\n helperText ? (\n <div\n id={helperTextId}\n className=\"text-[length:var(--malachite-font-size-small,13px)] text-left pl-2 text-[var(--malachite-text-dimmed,rgb(161,161,161))]\"\n >\n {helperText}\n </div>\n ) : null;\n\n const renderErrorMessage = (errorMessage?: string) => (\n <FieldMessage id={errorMessageId} message={errorMessage} spaced />\n );\n\n return (\n <BaseFormComponent\n label={label}\n hint={hint}\n tooltip={tooltip}\n name={name}\n required={required}\n requiredIndicator={requiredIndicator}\n loading={loading}\n formControl={formControl}\n description={description}\n controlId={controlId}\n labelId={labelId}\n descriptionId={descriptionId}\n hintId={hintId}\n loadingComponent={<LoadingTextArea size={size} rows={rows} />}\n >\n {({ error: fieldError, message, onFieldChange, onFieldBlur, value: formValue }) => {\n const isError = error ?? fieldError;\n // `error={false}` opts out of the resolver entirely, message included: a\n // screen that has dealt with the rejection its own way would otherwise keep\n // reporting it in destructive text and through `aria-describedby`.\n\n // The message is its own line rather than a replacement for the helper text:\n // a field that explains what it wants still has to say why what was typed was\n // rejected, and dropping one for the other left the field marked red with\n // nothing anywhere saying what was wrong.\n const errorMessage = error === false ? undefined : message;\n\n return (\n <>\n {renderTextArea(\n isError,\n helperText,\n onFieldChange,\n onFieldBlur,\n errorMessage,\n formValue,\n )}\n {renderHelperText()}\n {renderErrorMessage(errorMessage)}\n </>\n );\n }}\n </BaseFormComponent>\n );\n};\n\nconst LoadingTextArea: React.FC<{ size?: TextAreaSize; rows?: number }> = ({\n size = 'medium',\n rows = 4,\n}) => {\n return (\n // Decorative height placeholder — kept out of the a11y tree so it is not announced\n // as an unlabelled box (same as Skeleton and LoadingInput).\n <div\n aria-busy=\"true\"\n aria-hidden=\"true\"\n className=\"w-full rounded-xl border border-solid border-[var(--malachite-card-border)] bg-[var(--malachite-card-border)] flex items-center justify-center shadow-[var(--malachite-shadow-default)]\"\n style={{ minHeight: getMinHeight(size, rows) }}\n />\n );\n};\n\nconst CompositeIcon: React.FC<{\n prefixValue?: string;\n prefixSecondaryValue?: string;\n icon?: React.ReactNode;\n}> = ({ prefixValue, prefixSecondaryValue, icon }) => {\n return (\n <div className=\"flex items-start gap-1\">\n {icon && (\n <span className=\"flex items-center justify-center text-[var(--malachite-text-dimmed)] w-4 h-4 flex-shrink-0 mt-1\">\n {icon}\n </span>\n )}\n {prefixValue && (\n <span className=\"text-[length:var(--malachite-font-size-15,15px)] leading-[1.4] text-[var(--malachite-text-dimmed)] whitespace-nowrap\">\n {prefixValue}\n </span>\n )}\n {prefixSecondaryValue && (\n <span className=\"text-[length:var(--malachite-font-size-15,15px)] leading-[1.4] text-[var(--malachite-text-dimmed)] whitespace-nowrap\">\n {prefixSecondaryValue}\n </span>\n )}\n </div>\n );\n};\n\nconst Suffix: React.FC<{\n error?: boolean;\n disabled?: boolean;\n defaultValue?: string;\n onCopyClick?: () => void;\n}> = ({ error, disabled, defaultValue, onCopyClick }) => {\n return (\n <div className=\"flex items-start gap-1\">\n {error && (\n // Decorative: the rejection is carried by the message beneath the field.\n <div\n aria-hidden=\"true\"\n className=\"flex items-center justify-center w-5 h-5 flex-shrink-0 mt-1\"\n >\n <InfoIcon\n size={16}\n className=\"text-[var(--malachite-destructive-foreground,rgb(196,62,62))]\"\n />\n </div>\n )}\n {onCopyClick && disabled && defaultValue && (\n <button\n onClick={() => {\n navigator.clipboard.writeText(defaultValue);\n onCopyClick();\n }}\n type=\"button\"\n aria-label=\"Copy to clipboard\"\n className=\"flex items-center justify-center w-5 h-5 hover:bg-[var(--malachite-primary-border)] rounded transition-colors flex-shrink-0 mt-1\"\n >\n <CopyIcon size={16} />\n </button>\n )}\n </div>\n );\n};\n\nconst TextAreaButtons: React.FC<{\n buttons?: ButtonDefinition[];\n}> = ({ buttons }) => {\n if (!buttons?.length) {\n return null;\n }\n\n return (\n <div className=\"flex flex-col gap-1 mt-1\">\n {buttons.map((button) => (\n <Button\n key={button.label}\n variant={button.danger ? 'destructive' : 'outline'}\n size=\"small\"\n disabled={button.disabled}\n icon={button.icon}\n tooltip={button.tooltip}\n onClick={button.onClick}\n >\n {button.label}\n </Button>\n ))}\n </div>\n );\n};\n\nexport default TextArea;\n"],"names":["getSizeClasses","size","focusBorderClasses","isError","tone","rowHeightPx","rows","getMinHeight","TextArea","label","hint","tooltip","placeholder","icon","name","required","requiredIndicator","error","description","loading","readOnly","defaultValue","disabled","prefixValue","prefixSecondaryValue","formControl","buttons","onCopyClick","onChange","onKeyDown","inFieldAction","value","onBlur","helperText","maxRows","maxLength","resize","wrap","id","ariaLabel","ariaLabelledBy","controlId","labelId","descriptionId","hintId","helperTextId","useFormFieldIds","internalValue","setInternalValue","useState","textareaRef","useRef","resizeToFit","useCallback","el","maxHeight","nextHeight","useLayoutEffect","isControlled","paintedValue","formValue","paintedFieldValue","handleChange","reportToForm","e","newValue","hasPrefix","errorMessageId","renderTextArea","resolvedHelperText","onFieldChange","onFieldBlur","errorMessage","textAreaValue","showErrorIcon","hasSuffix","describedBy","wrapperClasses","textAreaClasses","textAreaElement","jsx","jsxs","CompositeIcon","Suffix","TextAreaButtons","renderHelperText","renderErrorMessage","FieldMessage","BaseFormComponent","LoadingTextArea","fieldError","message","Fragment","InfoIcon","CopyIcon","button","Button"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TextArea.js","sources":["../../../src/components/TextArea/TextArea.tsx"],"sourcesContent":["import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';\nimport type { Control, FieldPath, FieldValues } from 'react-hook-form';\nimport BaseFormComponent, { type RequiredIndicator, useFormFieldIds } from '../BaseFormComponent';\nimport { Button } from '../Button';\nimport CopyIcon from '../CustomIcons/CopyIcon';\nimport InfoIcon from '../CustomIcons/InfoIcon';\nimport FieldMessage from '../FieldMessage';\nimport { paintedFieldValue } from '../devWarning';\nimport type { ButtonDefinition, TextAreaSize } from './types';\n\n/**\n * `assistant` is the chrome for a field sitting in an AI context (e.g. `Chat.Composer`'s\n * policy-assistant tone): the field's own focus border recolours to the assistant violet\n * instead of the default accent — same indicator, different colour, not an extra one. Same\n * naming precedent as `PillButton`'s own `tone`.\n */\ntype TextAreaTone = 'default' | 'assistant';\n\ninterface TextAreaProps {\n label?: string;\n hint?: string;\n tooltip?: string;\n placeholder?: string;\n icon?: React.ReactNode;\n readOnly?: boolean;\n name: FieldPath<FieldValues> | string;\n description?: string;\n size?: TextAreaSize;\n required?: boolean;\n requiredIndicator?: RequiredIndicator;\n error?: boolean;\n loading?: boolean;\n defaultValue?: string;\n disabled?: boolean;\n prefixValue?: string;\n prefixSecondaryValue?: string;\n ellipsis?: boolean;\n formControl?: Control<FieldValues, unknown>;\n buttons?: ButtonDefinition[];\n onCopyClick?: () => void;\n onChange?: (value: string) => void;\n value?: string;\n onBlur?: () => void;\n /** Key handling the control cannot own, e.g. Enter-to-submit in a chat composer. */\n onKeyDown?: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;\n /**\n * A control that belongs inside the field, pinned bottom-right — a chat composer's send.\n * The field reserves room for it, so text cannot run underneath.\n */\n inFieldAction?: React.ReactNode;\n /** See `TextAreaTone`. Defaults to `'default'`, so no existing field changes. */\n tone?: TextAreaTone;\n helperText?: React.ReactNode;\n rows?: number;\n /**\n * Grows the field from `rows` up to this many lines as content is typed, then scrolls\n * internally rather than growing further. Omitted keeps the field at a fixed `rows`\n * height, the pre-existing behaviour.\n */\n maxRows?: number;\n maxLength?: number;\n resize?: 'none' | 'vertical' | 'horizontal' | 'both';\n /**\n * When false, lines are not soft-wrapped — long content scrolls horizontally\n * instead of wrapping (native `wrap=\"off\"`). Useful for one-value-per-line\n * content like URLs. Defaults to true.\n */\n wrap?: boolean;\n id?: string;\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n}\n\nconst getSizeClasses = (size: TextAreaSize): string => {\n switch (size) {\n case 'small':\n return 'text-sm';\n case 'medium':\n return 'text-[length:var(--malachite-font-size-15,15px)] leading-[1.4]';\n case 'large':\n return 'text-base';\n default:\n return 'text-[length:var(--malachite-font-size-15,15px)] leading-[1.4]';\n }\n};\n\n/**\n * A rejection always wins the border colour, regardless of `tone`: a field the caller has\n * flagged invalid stays destructive-red on focus even inside an assistant surface.\n *\n * Each branch spells its two classes out in full rather than interpolating a shared colour\n * variable into them. Tailwind extracts candidates by scanning source text, so a class built\n * at runtime is one Tailwind never sees — and the CSS is simply absent, with no build error.\n * The assistant pair was surviving only because the same literal appears in a spec's\n * assertion, which the content glob also scans: rewording that assertion would have silently\n * dropped the violet focus border.\n */\nconst focusBorderClasses = (isError: boolean, tone: TextAreaTone): string => {\n if (isError) {\n return 'focus-within:border-[var(--malachite-destructive-border-hover)] hover:border-[var(--malachite-destructive-border-hover)]';\n }\n\n if (tone === 'assistant') {\n return 'focus-within:border-[var(--malachite-assistant-foreground)] hover:border-[var(--malachite-assistant-foreground)]';\n }\n\n return 'focus-within:border-[var(--malachite-control-border-hover)] hover:border-[var(--malachite-control-border-hover)]';\n};\n\n// Shared by the static min-height and the auto-grow ceiling, so a field's grown height\n// lines up with the same row metrics its resting height already used.\nconst rowHeightPx = (size: TextAreaSize, rows: number): number => {\n const baseHeight = {\n small: 20,\n medium: 22,\n large: 24,\n }[size];\n\n // Every size renders `py-[12px]`, so the vertical padding is 24 regardless of size.\n const padding = 24;\n\n return baseHeight * rows + padding;\n};\n\nconst getMinHeight = (size: TextAreaSize, rows: number): string => `${rowHeightPx(size, rows)}px`;\n\nconst TextArea: React.FC<TextAreaProps> = ({\n label,\n hint,\n tooltip,\n placeholder,\n icon,\n name,\n required,\n requiredIndicator,\n error,\n description,\n loading = false,\n readOnly = false,\n size = 'medium',\n defaultValue,\n disabled,\n prefixValue,\n prefixSecondaryValue,\n // ellipsis = false,\n formControl,\n buttons,\n onCopyClick,\n onChange,\n onKeyDown,\n inFieldAction,\n tone = 'default',\n value,\n onBlur,\n helperText,\n rows = 4,\n maxRows,\n maxLength,\n resize = 'vertical',\n wrap = true,\n id,\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n}) => {\n const { controlId, labelId, descriptionId, hintId, helperTextId } = useFormFieldIds({\n id,\n label,\n description,\n hint,\n helperText: true,\n });\n const [internalValue, setInternalValue] = useState(defaultValue || '');\n const textareaRef = useRef<HTMLTextAreaElement | null>(null);\n\n // Reset-then-measure rather than growing incrementally: `scrollHeight` on a field already\n // taller than its content stays stuck at that height, so the reset is what lets a paste\n // followed by a delete shrink the field back down, not just grow it. Mutates the DOM\n // directly instead of round-tripping through React state, so typing a character never\n // re-renders this component just to change its own height.\n const resizeToFit = useCallback(() => {\n const el = textareaRef.current;\n if (!el || maxRows === undefined) {\n return;\n }\n\n const maxHeight = rowHeightPx(size, maxRows);\n el.style.height = 'auto';\n const nextHeight = Math.min(el.scrollHeight, maxHeight);\n el.style.height = `${nextHeight}px`;\n el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden';\n }, [maxRows, size]);\n\n // Catches every change `onChange` on the element itself would miss: the initial mount,\n // and a value the parent sets from outside (e.g. clearing the composer after send).\n // biome-ignore lint/correctness/useExhaustiveDependencies: `value` is not read in the body, but a controlled `value` changing from outside is exactly the case `onChange` cannot see, so the effect has to re-run on it anyway.\n useLayoutEffect(() => {\n resizeToFit();\n }, [resizeToFit, value]);\n\n const isControlled = value !== undefined;\n const paintedValue = (formValue: unknown): string =>\n paintedFieldValue({\n component: 'TextArea',\n name,\n value,\n formValue,\n defaultValue,\n bound: !!formControl,\n uncontrolledValue: internalValue,\n });\n\n const handleChange =\n (reportToForm?: (value: unknown) => void) =>\n (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n const newValue = e.target.value;\n if (!isControlled) {\n setInternalValue(newValue);\n }\n // The browser has already applied the keystroke to the element by the time this\n // fires, so `scrollHeight` already reflects it — no need to wait for a re-render.\n resizeToFit();\n onChange?.(newValue);\n reportToForm?.(newValue);\n };\n\n const hasPrefix = icon || prefixValue || prefixSecondaryValue;\n\n const errorMessageId = `${helperTextId}-error`;\n\n const renderTextArea = (\n isError: boolean,\n resolvedHelperText?: React.ReactNode,\n onFieldChange?: (value: unknown) => void,\n onFieldBlur?: () => void,\n errorMessage?: string,\n formValue?: unknown,\n ) => {\n const textAreaValue = paintedValue(formValue);\n // The icon is the only signal a consumer-forced `error` has; beside a message it is\n // duplication, and it crowds the suffix next to a reveal or copy control.\n const showErrorIcon = isError && !errorMessage;\n const hasSuffix =\n showErrorIcon || (onCopyClick && disabled && textAreaValue) || buttons?.length;\n const describedBy =\n [\n descriptionId,\n hintId,\n resolvedHelperText ? helperTextId : undefined,\n errorMessage ? errorMessageId : undefined,\n ]\n .filter(Boolean)\n .join(' ') || undefined;\n const wrapperClasses = [\n 'relative',\n 'flex',\n 'w-full',\n 'rounded-xl',\n 'border border-solid',\n disabled\n ? 'bg-[var(--malachite-card-dimmed-background)]'\n : 'bg-[var(--malachite-card-background)]',\n 'transition-colors duration-200 ease-in-out',\n isError ? 'shadow-error' : 'shadow-default',\n isError\n ? 'border-[var(--malachite-destructive-border)]'\n : 'border-[var(--malachite-card-border)]',\n // One border-hover colour, not two overlapping utilities for the same states: a\n // duplicate pair used to sit above this one, fixed at the default accent — since\n // it targeted the identical `focus-within`/`hover` selectors, a `tone` swap here\n // would otherwise leave a second, conflicting colour in the class list.\n //\n // No separate ring on top of it: the recoloured border IS this field's focus\n // indicator, the same as the default tone's. Adding a ring here drew a second,\n // concentric outline around the first on `assistant` — the field already has one,\n // it only needed to change colour.\n disabled ? '' : focusBorderClasses(isError, tone),\n ]\n .filter(Boolean)\n .join(' ');\n\n const textAreaClasses = [\n 'flex-1',\n 'bg-transparent',\n 'border-none',\n 'outline-none',\n 'text-[var(--malachite-text-primary)]',\n 'placeholder:text-[var(--malachite-text-placeholder)]',\n 'disabled:text-[var(--malachite-text-dimmed)]',\n 'disabled:cursor-not-allowed',\n getSizeClasses(size),\n // Emitted as one side each when an action sits in the field: `px` and `pr` are\n // the same specificity, so which wins would rest on Tailwind's output order.\n inFieldAction ? 'pl-[16px] pr-[52px]' : 'px-[16px]',\n 'py-[12px]',\n resize === 'none'\n ? 'resize-none'\n : resize === 'vertical'\n ? 'resize-y'\n : resize === 'horizontal'\n ? 'resize-x'\n : 'resize',\n wrap ? '' : 'whitespace-pre overflow-x-auto min-w-0',\n ]\n .filter(Boolean)\n .join(' ');\n\n const textAreaElement = (\n <textarea\n ref={textareaRef}\n id={controlId}\n placeholder={placeholder}\n value={textAreaValue}\n onChange={handleChange(onFieldChange)}\n onBlur={() => {\n onBlur?.();\n onFieldBlur?.();\n }}\n onKeyDown={onKeyDown}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n aria-invalid={isError || undefined}\n aria-required={required || undefined}\n aria-describedby={describedBy}\n disabled={disabled || !!prefixValue || !!prefixSecondaryValue || readOnly}\n readOnly={readOnly}\n wrap={wrap ? undefined : 'off'}\n rows={rows}\n maxLength={maxLength}\n style={{ minHeight: getMinHeight(size, rows) }}\n className={textAreaClasses}\n name={name}\n />\n );\n\n return (\n <div className={wrapperClasses}>\n {hasPrefix && (\n <div className=\"flex pt-3 pl-3 pr-1 flex-shrink-0 pointer-events-none\">\n <CompositeIcon\n prefixValue={prefixValue}\n prefixSecondaryValue={prefixSecondaryValue}\n icon={icon}\n />\n </div>\n )}\n\n {textAreaElement}\n\n {inFieldAction && (\n <div className=\"absolute bottom-2 right-2 flex items-center\">\n {inFieldAction}\n </div>\n )}\n\n {hasSuffix && (\n <div className=\"flex pt-3 pl-1 pr-3 flex-shrink-0\">\n <Suffix\n error={showErrorIcon}\n disabled={disabled}\n defaultValue={textAreaValue}\n onCopyClick={onCopyClick}\n />\n <TextAreaButtons buttons={buttons} />\n </div>\n )}\n </div>\n );\n };\n\n const renderHelperText = () =>\n helperText ? (\n <div\n id={helperTextId}\n className=\"mt-1 text-[length:var(--malachite-font-size-small,13px)] text-left pl-2 text-[var(--malachite-text-dimmed,rgb(161,161,161))]\"\n >\n {helperText}\n </div>\n ) : null;\n\n const renderErrorMessage = (errorMessage?: string) => (\n <FieldMessage id={errorMessageId} message={errorMessage} spaced />\n );\n\n return (\n <BaseFormComponent\n label={label}\n hint={hint}\n tooltip={tooltip}\n name={name}\n required={required}\n requiredIndicator={requiredIndicator}\n loading={loading}\n formControl={formControl}\n description={description}\n controlId={controlId}\n labelId={labelId}\n descriptionId={descriptionId}\n hintId={hintId}\n loadingComponent={<LoadingTextArea size={size} rows={rows} />}\n >\n {({ error: fieldError, message, onFieldChange, onFieldBlur, value: formValue }) => {\n const isError = error ?? fieldError;\n // `error={false}` opts out of the resolver entirely, message included: a\n // screen that has dealt with the rejection its own way would otherwise keep\n // reporting it in destructive text and through `aria-describedby`.\n\n // The message is its own line rather than a replacement for the helper text:\n // a field that explains what it wants still has to say why what was typed was\n // rejected, and dropping one for the other left the field marked red with\n // nothing anywhere saying what was wrong.\n const errorMessage = error === false ? undefined : message;\n\n return (\n <>\n {renderTextArea(\n isError,\n helperText,\n onFieldChange,\n onFieldBlur,\n errorMessage,\n formValue,\n )}\n {renderHelperText()}\n {renderErrorMessage(errorMessage)}\n </>\n );\n }}\n </BaseFormComponent>\n );\n};\n\nconst LoadingTextArea: React.FC<{ size?: TextAreaSize; rows?: number }> = ({\n size = 'medium',\n rows = 4,\n}) => {\n return (\n // Decorative height placeholder — kept out of the a11y tree so it is not announced\n // as an unlabelled box (same as Skeleton and LoadingInput).\n <div\n aria-busy=\"true\"\n aria-hidden=\"true\"\n className=\"w-full rounded-xl border border-solid border-[var(--malachite-card-border)] bg-[var(--malachite-card-border)] flex items-center justify-center shadow-[var(--malachite-shadow-default)]\"\n style={{ minHeight: getMinHeight(size, rows) }}\n />\n );\n};\n\nconst CompositeIcon: React.FC<{\n prefixValue?: string;\n prefixSecondaryValue?: string;\n icon?: React.ReactNode;\n}> = ({ prefixValue, prefixSecondaryValue, icon }) => {\n return (\n <div className=\"flex items-start gap-1\">\n {icon && (\n <span className=\"flex items-center justify-center text-[var(--malachite-text-dimmed)] w-4 h-4 flex-shrink-0 mt-1\">\n {icon}\n </span>\n )}\n {prefixValue && (\n <span className=\"text-[length:var(--malachite-font-size-15,15px)] leading-[1.4] text-[var(--malachite-text-dimmed)] whitespace-nowrap\">\n {prefixValue}\n </span>\n )}\n {prefixSecondaryValue && (\n <span className=\"text-[length:var(--malachite-font-size-15,15px)] leading-[1.4] text-[var(--malachite-text-dimmed)] whitespace-nowrap\">\n {prefixSecondaryValue}\n </span>\n )}\n </div>\n );\n};\n\nconst Suffix: React.FC<{\n error?: boolean;\n disabled?: boolean;\n defaultValue?: string;\n onCopyClick?: () => void;\n}> = ({ error, disabled, defaultValue, onCopyClick }) => {\n return (\n <div className=\"flex items-start gap-1\">\n {error && (\n // Decorative: the rejection is carried by the message beneath the field.\n <div\n aria-hidden=\"true\"\n className=\"flex items-center justify-center w-5 h-5 flex-shrink-0 mt-1\"\n >\n <InfoIcon\n size={16}\n className=\"text-[var(--malachite-destructive-foreground,rgb(196,62,62))]\"\n />\n </div>\n )}\n {onCopyClick && disabled && defaultValue && (\n <button\n onClick={() => {\n navigator.clipboard.writeText(defaultValue);\n onCopyClick();\n }}\n type=\"button\"\n aria-label=\"Copy to clipboard\"\n className=\"flex items-center justify-center w-5 h-5 hover:bg-[var(--malachite-primary-border)] rounded transition-colors flex-shrink-0 mt-1\"\n >\n <CopyIcon size={16} />\n </button>\n )}\n </div>\n );\n};\n\nconst TextAreaButtons: React.FC<{\n buttons?: ButtonDefinition[];\n}> = ({ buttons }) => {\n if (!buttons?.length) {\n return null;\n }\n\n return (\n <div className=\"flex flex-col gap-1 mt-1\">\n {buttons.map((button) => (\n <Button\n key={button.label}\n variant={button.danger ? 'destructive' : 'outline'}\n size=\"small\"\n disabled={button.disabled}\n icon={button.icon}\n tooltip={button.tooltip}\n onClick={button.onClick}\n >\n {button.label}\n </Button>\n ))}\n </div>\n );\n};\n\nexport default TextArea;\n"],"names":["getSizeClasses","size","focusBorderClasses","isError","tone","rowHeightPx","rows","getMinHeight","TextArea","label","hint","tooltip","placeholder","icon","name","required","requiredIndicator","error","description","loading","readOnly","defaultValue","disabled","prefixValue","prefixSecondaryValue","formControl","buttons","onCopyClick","onChange","onKeyDown","inFieldAction","value","onBlur","helperText","maxRows","maxLength","resize","wrap","id","ariaLabel","ariaLabelledBy","controlId","labelId","descriptionId","hintId","helperTextId","useFormFieldIds","internalValue","setInternalValue","useState","textareaRef","useRef","resizeToFit","useCallback","el","maxHeight","nextHeight","useLayoutEffect","isControlled","paintedValue","formValue","paintedFieldValue","handleChange","reportToForm","e","newValue","hasPrefix","errorMessageId","renderTextArea","resolvedHelperText","onFieldChange","onFieldBlur","errorMessage","textAreaValue","showErrorIcon","hasSuffix","describedBy","wrapperClasses","textAreaClasses","textAreaElement","jsx","jsxs","CompositeIcon","Suffix","TextAreaButtons","renderHelperText","renderErrorMessage","FieldMessage","BaseFormComponent","LoadingTextArea","fieldError","message","Fragment","InfoIcon","CopyIcon","button","Button"],"mappings":"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"}
|
|
@@ -1,64 +1,65 @@
|
|
|
1
|
-
import { jsx as l, jsxs as
|
|
2
|
-
import E, { useState as x, useRef as
|
|
3
|
-
import { createPortal as
|
|
4
|
-
import { ensureOverlayStyles as
|
|
5
|
-
import { useTopLayer as
|
|
6
|
-
const
|
|
1
|
+
import { jsx as l, jsxs as H, Fragment as D } from "react/jsx-runtime";
|
|
2
|
+
import E, { useState as x, useRef as f, useCallback as J, useId as Q, useEffect as P, useInsertionEffect as Z } from "react";
|
|
3
|
+
import { createPortal as ee } from "react-dom";
|
|
4
|
+
import { ensureOverlayStyles as te, overlayIdentity as ne } from "./portalOverlay.js";
|
|
5
|
+
import { useTopLayer as re } from "./topLayer.js";
|
|
6
|
+
const oe = new RegExp("(?<=[/\\\\.\\-_?&#=:@])"), ie = (n) => n.split(oe).map((i, p) => /* @__PURE__ */ H(E.Fragment, { children: [
|
|
7
7
|
i,
|
|
8
8
|
/* @__PURE__ */ l("wbr", {})
|
|
9
|
-
] },
|
|
9
|
+
] }, p)), se = {
|
|
10
10
|
minWidth: 0,
|
|
11
11
|
overflow: "hidden"
|
|
12
|
-
},
|
|
12
|
+
}, C = {
|
|
13
13
|
flex: 1,
|
|
14
14
|
minWidth: 0
|
|
15
|
-
},
|
|
15
|
+
}, V = 8, pe = ({
|
|
16
16
|
text: n,
|
|
17
17
|
children: i,
|
|
18
|
-
delay:
|
|
19
|
-
wrapChildren:
|
|
20
|
-
triggerAs:
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
delay: p = 1,
|
|
19
|
+
wrapChildren: j = !1,
|
|
20
|
+
triggerAs: k = "div",
|
|
21
|
+
inline: R = !1,
|
|
22
|
+
placement: z = "top",
|
|
23
|
+
isShrinkable: L = !1,
|
|
23
24
|
maxWidth: m = "320px",
|
|
24
25
|
fill: h = !1,
|
|
25
26
|
shouldShow: c,
|
|
26
|
-
describesTrigger:
|
|
27
|
+
describesTrigger: T = !0
|
|
27
28
|
}) => {
|
|
28
|
-
const
|
|
29
|
+
const N = L || h ? { ...L ? se : {}, ...h ? C : {} } : void 0, [t, g] = x(!1), [d, O] = x(!1), u = f(null), r = f(null), o = J(() => {
|
|
29
30
|
u.current && (clearTimeout(u.current), u.current = null), r.current !== null && (cancelAnimationFrame(r.current), r.current = null);
|
|
30
|
-
}, []),
|
|
31
|
-
|
|
31
|
+
}, []), F = () => {
|
|
32
|
+
O(!0), r.current !== null && cancelAnimationFrame(r.current), r.current = requestAnimationFrame(() => {
|
|
32
33
|
r.current = null, g(!0);
|
|
33
34
|
});
|
|
34
|
-
}, v =
|
|
35
|
-
|
|
36
|
-
const
|
|
35
|
+
}, v = f(null), y = f(null);
|
|
36
|
+
re(v, t);
|
|
37
|
+
const I = Q(), s = n != null && n !== "" && n !== !1, Y = (e) => {
|
|
37
38
|
e.stopPropagation(), !(!s || c && !c()) && (o(), u.current = setTimeout(() => {
|
|
38
|
-
|
|
39
|
-
},
|
|
40
|
-
},
|
|
39
|
+
F();
|
|
40
|
+
}, p * 1e3));
|
|
41
|
+
}, U = (e) => {
|
|
41
42
|
e.stopPropagation(), o(), g(!1);
|
|
42
|
-
}, U = () => {
|
|
43
|
-
!s || c && !c() || (o(), T());
|
|
44
43
|
}, q = () => {
|
|
44
|
+
!s || c && !c() || (o(), F());
|
|
45
|
+
}, G = () => {
|
|
45
46
|
o(), g(!1);
|
|
46
|
-
}, [,
|
|
47
|
+
}, [, S] = x({});
|
|
47
48
|
P(() => {
|
|
48
49
|
if (!t)
|
|
49
50
|
return () => {
|
|
50
51
|
o();
|
|
51
52
|
};
|
|
52
53
|
const e = () => {
|
|
53
|
-
|
|
54
|
+
S({});
|
|
54
55
|
};
|
|
55
56
|
return window.addEventListener("scroll", e, !0), window.addEventListener("resize", e), () => {
|
|
56
57
|
o(), window.removeEventListener("scroll", e, !0), window.removeEventListener("resize", e);
|
|
57
58
|
};
|
|
58
59
|
}, [t, o]), P(() => {
|
|
59
|
-
t &&
|
|
60
|
+
t && S({});
|
|
60
61
|
}, [t]);
|
|
61
|
-
const
|
|
62
|
+
const K = () => {
|
|
62
63
|
if (!y.current)
|
|
63
64
|
return { display: "none" };
|
|
64
65
|
const e = y.current.getBoundingClientRect(), a = {
|
|
@@ -84,14 +85,14 @@ const re = new RegExp("(?<=[/\\\\.\\-_?&#=:@])"), oe = (n) => n.split(re).map((i
|
|
|
84
85
|
visibility: t ? "visible" : "hidden",
|
|
85
86
|
transition: "opacity 150ms ease-in-out, transform 150ms ease-in-out, visibility 150ms ease-in-out"
|
|
86
87
|
}, b = () => {
|
|
87
|
-
var
|
|
88
|
-
const w = e.left + e.width / 2,
|
|
89
|
-
if (
|
|
88
|
+
var $;
|
|
89
|
+
const w = e.left + e.width / 2, B = (($ = v.current) == null ? void 0 : $.getBoundingClientRect().width) ?? 0;
|
|
90
|
+
if (B === 0)
|
|
90
91
|
return w;
|
|
91
|
-
const
|
|
92
|
-
return
|
|
92
|
+
const M = B / 2, W = V + M, _ = window.innerWidth - V - M;
|
|
93
|
+
return W > _ ? w : Math.min(Math.max(w, W), _);
|
|
93
94
|
};
|
|
94
|
-
switch (
|
|
95
|
+
switch (z) {
|
|
95
96
|
case "top":
|
|
96
97
|
return {
|
|
97
98
|
...a,
|
|
@@ -128,37 +129,37 @@ const re = new RegExp("(?<=[/\\\\.\\-_?&#=:@])"), oe = (n) => n.split(re).map((i
|
|
|
128
129
|
transform: `translate(-50%, -100%) ${t ? "scale(1)" : "scale(0.95)"}`
|
|
129
130
|
};
|
|
130
131
|
}
|
|
131
|
-
},
|
|
132
|
-
|
|
133
|
-
s && d &&
|
|
132
|
+
}, A = s && d && T ? I : void 0;
|
|
133
|
+
Z(() => {
|
|
134
|
+
s && d && te();
|
|
134
135
|
}, [s, d]);
|
|
135
|
-
const
|
|
136
|
-
"aria-describedby": [i.props["aria-describedby"],
|
|
136
|
+
const X = j || !E.isValidElement(i) ? /* @__PURE__ */ l("span", { "aria-describedby": A, style: h ? C : void 0, children: i }) : E.cloneElement(i, {
|
|
137
|
+
"aria-describedby": [i.props["aria-describedby"], A].filter(Boolean).join(" ") || void 0
|
|
137
138
|
});
|
|
138
|
-
return /* @__PURE__ */
|
|
139
|
+
return /* @__PURE__ */ H(D, { children: [
|
|
139
140
|
/* @__PURE__ */ l(
|
|
140
|
-
|
|
141
|
+
R ? "span" : k,
|
|
141
142
|
{
|
|
142
143
|
ref: y,
|
|
143
|
-
className:
|
|
144
|
-
style:
|
|
145
|
-
onMouseEnter:
|
|
146
|
-
onMouseLeave:
|
|
147
|
-
onFocus:
|
|
148
|
-
onBlur:
|
|
149
|
-
children:
|
|
144
|
+
className: `relative ${R ? "inline-flex max-w-full align-middle" : "flex"} items-center`,
|
|
145
|
+
style: N,
|
|
146
|
+
onMouseEnter: Y,
|
|
147
|
+
onMouseLeave: U,
|
|
148
|
+
onFocus: q,
|
|
149
|
+
onBlur: G,
|
|
150
|
+
children: X
|
|
150
151
|
}
|
|
151
152
|
),
|
|
152
|
-
s && d &&
|
|
153
|
+
s && d && ee(
|
|
153
154
|
/* @__PURE__ */ l(
|
|
154
155
|
"div",
|
|
155
156
|
{
|
|
156
157
|
ref: v,
|
|
157
|
-
id:
|
|
158
|
+
id: I,
|
|
158
159
|
role: "tooltip",
|
|
159
|
-
"aria-hidden":
|
|
160
|
-
...
|
|
161
|
-
style:
|
|
160
|
+
"aria-hidden": T ? void 0 : !0,
|
|
161
|
+
...ne("tooltip"),
|
|
162
|
+
style: K(),
|
|
162
163
|
children: /* @__PURE__ */ l(
|
|
163
164
|
"span",
|
|
164
165
|
{
|
|
@@ -166,7 +167,7 @@ const re = new RegExp("(?<=[/\\\\.\\-_?&#=:@])"), oe = (n) => n.split(re).map((i
|
|
|
166
167
|
minWidth: 0,
|
|
167
168
|
overflowWrap: "anywhere"
|
|
168
169
|
},
|
|
169
|
-
children: typeof n == "string" ?
|
|
170
|
+
children: typeof n == "string" ? ie(n) : n
|
|
170
171
|
}
|
|
171
172
|
)
|
|
172
173
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tooltip.js","sources":["../../src/components/Tooltip.tsx"],"sourcesContent":["import React, { useState, useRef, useEffect, useId, useInsertionEffect, useCallback } from 'react';\nimport { createPortal } from 'react-dom';\nimport { ensureOverlayStyles, overlayIdentity } from './portalOverlay';\nimport { useTopLayer } from './topLayer';\n\n// Split after each delimiter so long values (URLs, dotted paths) wrap at \"/\" or \".\"\n// instead of mid-token. <wbr> is copy-safe (a zero-width space would corrupt a copied\n// URL); overflow-wrap still handles a single run too long to fit.\nconst WRAP_AFTER = /(?<=[/\\\\.\\-_?&#=:@])/;\nconst withBreakOpportunities = (value: string): React.ReactNode =>\n value.split(WRAP_AFTER).map((chunk, i) => (\n <React.Fragment key={i}>\n {chunk}\n <wbr />\n </React.Fragment>\n ));\n\ninterface TooltipProps {\n text: React.ReactNode | string;\n children: React.ReactNode;\n delay?: number;\n wrapChildren?: boolean;\n placement?: 'top' | 'bottom' | 'left' | 'right';\n isShrinkable?: boolean;\n fill?: boolean;\n /** Max width of tooltip. Default: 320px */\n maxWidth?: number | string;\n /** Called synchronously when the trigger is hovered/focused; return false to\n * suppress the tooltip for this interaction. Lets wrappers defer expensive\n * checks (e.g. truncation measurement) to interaction time instead of mount. */\n shouldShow?: () => boolean;\n triggerAs?: 'div' | 'span';\n /** Whether the tooltip acts as the trigger's accessible description. False where the\n * trigger already points at the same sentence on screen, so it is not announced twice. */\n describesTrigger?: boolean;\n}\n\nconst SHRINKABLE_STYLE: React.CSSProperties = {\n minWidth: 0,\n overflow: 'hidden',\n};\n\nconst FILL_STYLE: React.CSSProperties = {\n flex: 1,\n minWidth: 0,\n};\n\n/** Kept clear of the viewport edge so a clamped tooltip does not sit flush against it. */\nconst VIEWPORT_MARGIN = 8;\n\nconst Tooltip: React.FC<TooltipProps> = ({\n text,\n children,\n delay = 1,\n wrapChildren = false,\n triggerAs = 'div',\n placement = 'top',\n isShrinkable = false,\n maxWidth = '320px',\n fill = false,\n shouldShow,\n describesTrigger = true,\n}) => {\n const triggerStyle =\n isShrinkable || fill\n ? { ...(isShrinkable ? SHRINKABLE_STYLE : {}), ...(fill ? FILL_STYLE : {}) }\n : undefined;\n\n const [isVisible, setIsVisible] = useState(false);\n // Portal mounts on first show, not on mount: an at-rest tooltip otherwise\n // costs a hidden document.body node plus a getBoundingClientRect per render —\n // multiplied by every row in a large table.\n const [hasShown, setHasShown] = useState(false);\n const timeoutRef = useRef<NodeJS.Timeout | null>(null);\n const showFrameRef = useRef<number | null>(null);\n\n const cancelPendingShow = useCallback(() => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n }\n if (showFrameRef.current !== null) {\n cancelAnimationFrame(showFrameRef.current);\n showFrameRef.current = null;\n }\n }, []);\n\n const scheduleVisible = () => {\n // Two steps so the portal mounts hidden first and the fade-in transition\n // still runs on the very first show. The frame is tracked so leave/blur/\n // unmount can cancel a show that is already past its delay.\n setHasShown(true);\n if (showFrameRef.current !== null) {\n cancelAnimationFrame(showFrameRef.current);\n }\n showFrameRef.current = requestAnimationFrame(() => {\n showFrameRef.current = null;\n setIsVisible(true);\n });\n };\n const tooltipRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLDivElement>(null);\n\n // Tied to visibility, not mount: the node is latched into the DOM after its first show, and\n // re-entering on each show keeps a tooltip above the panel it is annotating.\n useTopLayer(tooltipRef, isVisible);\n\n const tooltipId = useId();\n\n const hasText = text !== null && text !== undefined && text !== '' && text !== false;\n\n const handleMouseEnter = (e: React.MouseEvent) => {\n e.stopPropagation();\n\n if (!hasText || (shouldShow && !shouldShow())) {\n return;\n }\n\n cancelPendingShow();\n\n timeoutRef.current = setTimeout(() => {\n scheduleVisible();\n }, delay * 1000);\n };\n\n const handleMouseLeave = (e: React.MouseEvent) => {\n e.stopPropagation();\n\n cancelPendingShow();\n setIsVisible(false);\n };\n\n const handleFocus = () => {\n if (!hasText || (shouldShow && !shouldShow())) {\n return;\n }\n\n cancelPendingShow();\n scheduleVisible();\n };\n\n const handleBlur = () => {\n cancelPendingShow();\n setIsVisible(false);\n };\n\n const [, forceUpdate] = useState({});\n\n useEffect(() => {\n // Only a VISIBLE tooltip needs to track scroll/resize; keeping global\n // listeners per mounted instance made every scroll event fan out across\n // every tooltip on the page.\n if (!isVisible) {\n return () => {\n cancelPendingShow();\n };\n }\n const handleUpdate = () => {\n forceUpdate({});\n };\n\n window.addEventListener('scroll', handleUpdate, true);\n window.addEventListener('resize', handleUpdate);\n\n return () => {\n cancelPendingShow();\n window.removeEventListener('scroll', handleUpdate, true);\n window.removeEventListener('resize', handleUpdate);\n };\n }, [isVisible, cancelPendingShow]);\n\n // The first render that mounts the tooltip has no measured width yet, so the clamp below\n // cannot run on it. One extra pass once it is in the DOM gives the clamp its measurement\n // — before the opacity transition finishes, so nothing is seen sliding into place.\n useEffect(() => {\n if (isVisible) {\n forceUpdate({});\n }\n }, [isVisible]);\n\n const getTooltipStyles = (): React.CSSProperties => {\n if (!triggerRef.current) {\n return { display: 'none' };\n }\n\n const rect = triggerRef.current.getBoundingClientRect();\n\n const baseStyles: React.CSSProperties = {\n position: 'fixed',\n display: 'flex',\n width: 'max-content',\n maxWidth: typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth,\n minWidth: '32px',\n minHeight: '32px',\n padding: '8px 12px',\n alignItems: 'center',\n gap: '8px',\n borderRadius: '12px',\n backgroundColor: 'var(--malachite-tooltip-background, rgb(59, 59, 59))',\n color: 'var(--malachite-tooltip-foreground, rgb(255, 255, 255))',\n boxShadow: 'var(--malachite-tooltip-shadow, 0px 0px 10px 0px rgba(0, 0, 0, 0.20))',\n fontSize: '14px',\n lineHeight: '1.4',\n whiteSpace: 'normal',\n textAlign: 'left',\n pointerEvents: 'none',\n opacity: isVisible ? 1 : 0,\n visibility: isVisible ? 'visible' : 'hidden',\n transition:\n 'opacity 150ms ease-in-out, transform 150ms ease-in-out, visibility 150ms ease-in-out',\n };\n\n // A trigger near the viewport edge would otherwise centre a wide tooltip half off\n // screen, where the panel or the window crops it. Clamping the CENTRE keeps the\n // `translateX(-50%)` placement honest: the box stays put, it just stops short.\n const centeredLeft = (): number => {\n const desired = rect.left + rect.width / 2;\n const width = tooltipRef.current?.getBoundingClientRect().width ?? 0;\n\n if (width === 0) {\n return desired;\n }\n\n const half = width / 2;\n const min = VIEWPORT_MARGIN + half;\n const max = window.innerWidth - VIEWPORT_MARGIN - half;\n\n // A tooltip wider than the viewport has no in-bounds centre; leave it centred\n // rather than pinning it to a nonsensical one.\n return min > max ? desired : Math.min(Math.max(desired, min), max);\n };\n\n switch (placement) {\n case 'top':\n return {\n ...baseStyles,\n top: rect.top - 8,\n left: centeredLeft(),\n transform: `translate(-50%, -100%) ${isVisible ? 'scale(1)' : 'scale(0.95)'}`,\n };\n case 'bottom':\n return {\n ...baseStyles,\n top: rect.bottom + 8,\n left: centeredLeft(),\n transform: `translateX(-50%) ${isVisible ? 'scale(1)' : 'scale(0.95)'}`,\n };\n case 'left':\n return {\n ...baseStyles,\n top: rect.top + rect.height / 2,\n left: rect.left - 8,\n transform: `translate(-100%, -50%) ${isVisible ? 'scale(1)' : 'scale(0.95)'}`,\n };\n case 'right':\n return {\n ...baseStyles,\n top: rect.top + rect.height / 2,\n left: rect.right + 8,\n transform: `translateY(-50%) ${isVisible ? 'scale(1)' : 'scale(0.95)'}`,\n };\n default:\n return {\n ...baseStyles,\n top: rect.top - 8,\n left: centeredLeft(),\n transform: `translate(-50%, -100%) ${isVisible ? 'scale(1)' : 'scale(0.95)'}`,\n };\n }\n };\n\n // Only reference the tooltip id once the portal can exist — a dangling\n // aria-describedby (text set, never shown) confuses assistive tech.\n const describedBy = hasText && hasShown && describesTrigger ? tooltipId : undefined;\n useInsertionEffect(() => {\n if (hasText && hasShown) {\n ensureOverlayStyles();\n }\n }, [hasText, hasShown]);\n const renderedChildren =\n wrapChildren || !React.isValidElement(children) ? (\n <span aria-describedby={describedBy} style={fill ? FILL_STYLE : undefined}>\n {children}\n </span>\n ) : (\n React.cloneElement(children as React.ReactElement, {\n 'aria-describedby':\n [(children as React.ReactElement).props['aria-describedby'], describedBy]\n .filter(Boolean)\n .join(' ') || undefined,\n })\n );\n\n const Trigger = triggerAs;\n\n // Create the tooltip content without its own shadow root\n return (\n <>\n <Trigger\n ref={triggerRef}\n className=\"relative flex items-center\"\n style={triggerStyle}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n onFocus={handleFocus}\n onBlur={handleBlur}\n >\n {renderedChildren}\n </Trigger>\n\n {hasText &&\n hasShown &&\n createPortal(\n <div\n ref={tooltipRef}\n id={tooltipId}\n role=\"tooltip\"\n // Nothing references it, and the sentence is already on screen and\n // described: left in the tree it is a second copy to read past.\n aria-hidden={describesTrigger ? undefined : true}\n {...overlayIdentity('tooltip')}\n style={getTooltipStyles()}\n >\n {/* min-w-0 + overflow-wrap let long values wrap within maxWidth. */}\n <span\n style={{\n minWidth: 0,\n overflowWrap: 'anywhere',\n }}\n >\n {typeof text === 'string' ? withBreakOpportunities(text) : text}\n </span>\n </div>,\n document.body,\n )}\n </>\n );\n};\n\nexport default Tooltip;\n"],"names":["WRAP_AFTER","withBreakOpportunities","value","chunk","i","jsxs","React","SHRINKABLE_STYLE","FILL_STYLE","VIEWPORT_MARGIN","Tooltip","text","children","delay","wrapChildren","triggerAs","placement","isShrinkable","maxWidth","fill","shouldShow","describesTrigger","triggerStyle","isVisible","setIsVisible","useState","hasShown","setHasShown","timeoutRef","useRef","showFrameRef","cancelPendingShow","useCallback","scheduleVisible","tooltipRef","triggerRef","useTopLayer","tooltipId","useId","hasText","handleMouseEnter","handleMouseLeave","handleFocus","handleBlur","forceUpdate","useEffect","handleUpdate","getTooltipStyles","rect","baseStyles","centeredLeft","desired","width","_a","half","min","max","describedBy","useInsertionEffect","ensureOverlayStyles","renderedChildren","jsx","Fragment","createPortal","overlayIdentity"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Tooltip.js","sources":["../../src/components/Tooltip.tsx"],"sourcesContent":["import React, { useState, useRef, useEffect, useId, useInsertionEffect, useCallback } from 'react';\nimport { createPortal } from 'react-dom';\nimport { ensureOverlayStyles, overlayIdentity } from './portalOverlay';\nimport { useTopLayer } from './topLayer';\n\n// Split after each delimiter so long values (URLs, dotted paths) wrap at \"/\" or \".\"\n// instead of mid-token. <wbr> is copy-safe (a zero-width space would corrupt a copied\n// URL); overflow-wrap still handles a single run too long to fit.\nconst WRAP_AFTER = /(?<=[/\\\\.\\-_?&#=:@])/;\nconst withBreakOpportunities = (value: string): React.ReactNode =>\n value.split(WRAP_AFTER).map((chunk, i) => (\n <React.Fragment key={i}>\n {chunk}\n <wbr />\n </React.Fragment>\n ));\n\ninterface TooltipProps {\n text: React.ReactNode | string;\n children: React.ReactNode;\n delay?: number;\n wrapChildren?: boolean;\n placement?: 'top' | 'bottom' | 'left' | 'right';\n isShrinkable?: boolean;\n fill?: boolean;\n /** Max width of tooltip. Default: 320px */\n maxWidth?: number | string;\n /** Called synchronously when the trigger is hovered/focused; return false to\n * suppress the tooltip for this interaction. Lets wrappers defer expensive\n * checks (e.g. truncation measurement) to interaction time instead of mount. */\n shouldShow?: () => boolean;\n triggerAs?: 'div' | 'span';\n inline?: boolean;\n /** Whether the tooltip acts as the trigger's accessible description. False where the\n * trigger already points at the same sentence on screen, so it is not announced twice. */\n describesTrigger?: boolean;\n}\n\nconst SHRINKABLE_STYLE: React.CSSProperties = {\n minWidth: 0,\n overflow: 'hidden',\n};\n\nconst FILL_STYLE: React.CSSProperties = {\n flex: 1,\n minWidth: 0,\n};\n\n/** Kept clear of the viewport edge so a clamped tooltip does not sit flush against it. */\nconst VIEWPORT_MARGIN = 8;\n\nconst Tooltip: React.FC<TooltipProps> = ({\n text,\n children,\n delay = 1,\n wrapChildren = false,\n triggerAs = 'div',\n inline = false,\n placement = 'top',\n isShrinkable = false,\n maxWidth = '320px',\n fill = false,\n shouldShow,\n describesTrigger = true,\n}) => {\n const triggerStyle =\n isShrinkable || fill\n ? { ...(isShrinkable ? SHRINKABLE_STYLE : {}), ...(fill ? FILL_STYLE : {}) }\n : undefined;\n\n const [isVisible, setIsVisible] = useState(false);\n // Portal mounts on first show, not on mount: an at-rest tooltip otherwise\n // costs a hidden document.body node plus a getBoundingClientRect per render —\n // multiplied by every row in a large table.\n const [hasShown, setHasShown] = useState(false);\n const timeoutRef = useRef<NodeJS.Timeout | null>(null);\n const showFrameRef = useRef<number | null>(null);\n\n const cancelPendingShow = useCallback(() => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n timeoutRef.current = null;\n }\n if (showFrameRef.current !== null) {\n cancelAnimationFrame(showFrameRef.current);\n showFrameRef.current = null;\n }\n }, []);\n\n const scheduleVisible = () => {\n // Two steps so the portal mounts hidden first and the fade-in transition\n // still runs on the very first show. The frame is tracked so leave/blur/\n // unmount can cancel a show that is already past its delay.\n setHasShown(true);\n if (showFrameRef.current !== null) {\n cancelAnimationFrame(showFrameRef.current);\n }\n showFrameRef.current = requestAnimationFrame(() => {\n showFrameRef.current = null;\n setIsVisible(true);\n });\n };\n const tooltipRef = useRef<HTMLDivElement>(null);\n const triggerRef = useRef<HTMLDivElement>(null);\n\n // Tied to visibility, not mount: the node is latched into the DOM after its first show, and\n // re-entering on each show keeps a tooltip above the panel it is annotating.\n useTopLayer(tooltipRef, isVisible);\n\n const tooltipId = useId();\n\n const hasText = text !== null && text !== undefined && text !== '' && text !== false;\n\n const handleMouseEnter = (e: React.MouseEvent) => {\n e.stopPropagation();\n\n if (!hasText || (shouldShow && !shouldShow())) {\n return;\n }\n\n cancelPendingShow();\n\n timeoutRef.current = setTimeout(() => {\n scheduleVisible();\n }, delay * 1000);\n };\n\n const handleMouseLeave = (e: React.MouseEvent) => {\n e.stopPropagation();\n\n cancelPendingShow();\n setIsVisible(false);\n };\n\n const handleFocus = () => {\n if (!hasText || (shouldShow && !shouldShow())) {\n return;\n }\n\n cancelPendingShow();\n scheduleVisible();\n };\n\n const handleBlur = () => {\n cancelPendingShow();\n setIsVisible(false);\n };\n\n const [, forceUpdate] = useState({});\n\n useEffect(() => {\n // Only a VISIBLE tooltip needs to track scroll/resize; keeping global\n // listeners per mounted instance made every scroll event fan out across\n // every tooltip on the page.\n if (!isVisible) {\n return () => {\n cancelPendingShow();\n };\n }\n const handleUpdate = () => {\n forceUpdate({});\n };\n\n window.addEventListener('scroll', handleUpdate, true);\n window.addEventListener('resize', handleUpdate);\n\n return () => {\n cancelPendingShow();\n window.removeEventListener('scroll', handleUpdate, true);\n window.removeEventListener('resize', handleUpdate);\n };\n }, [isVisible, cancelPendingShow]);\n\n // The first render that mounts the tooltip has no measured width yet, so the clamp below\n // cannot run on it. One extra pass once it is in the DOM gives the clamp its measurement\n // — before the opacity transition finishes, so nothing is seen sliding into place.\n useEffect(() => {\n if (isVisible) {\n forceUpdate({});\n }\n }, [isVisible]);\n\n const getTooltipStyles = (): React.CSSProperties => {\n if (!triggerRef.current) {\n return { display: 'none' };\n }\n\n const rect = triggerRef.current.getBoundingClientRect();\n\n const baseStyles: React.CSSProperties = {\n position: 'fixed',\n display: 'flex',\n width: 'max-content',\n maxWidth: typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth,\n minWidth: '32px',\n minHeight: '32px',\n padding: '8px 12px',\n alignItems: 'center',\n gap: '8px',\n borderRadius: '12px',\n backgroundColor: 'var(--malachite-tooltip-background, rgb(59, 59, 59))',\n color: 'var(--malachite-tooltip-foreground, rgb(255, 255, 255))',\n boxShadow: 'var(--malachite-tooltip-shadow, 0px 0px 10px 0px rgba(0, 0, 0, 0.20))',\n fontSize: '14px',\n lineHeight: '1.4',\n whiteSpace: 'normal',\n textAlign: 'left',\n pointerEvents: 'none',\n opacity: isVisible ? 1 : 0,\n visibility: isVisible ? 'visible' : 'hidden',\n transition:\n 'opacity 150ms ease-in-out, transform 150ms ease-in-out, visibility 150ms ease-in-out',\n };\n\n // A trigger near the viewport edge would otherwise centre a wide tooltip half off\n // screen, where the panel or the window crops it. Clamping the CENTRE keeps the\n // `translateX(-50%)` placement honest: the box stays put, it just stops short.\n const centeredLeft = (): number => {\n const desired = rect.left + rect.width / 2;\n const width = tooltipRef.current?.getBoundingClientRect().width ?? 0;\n\n if (width === 0) {\n return desired;\n }\n\n const half = width / 2;\n const min = VIEWPORT_MARGIN + half;\n const max = window.innerWidth - VIEWPORT_MARGIN - half;\n\n // A tooltip wider than the viewport has no in-bounds centre; leave it centred\n // rather than pinning it to a nonsensical one.\n return min > max ? desired : Math.min(Math.max(desired, min), max);\n };\n\n switch (placement) {\n case 'top':\n return {\n ...baseStyles,\n top: rect.top - 8,\n left: centeredLeft(),\n transform: `translate(-50%, -100%) ${isVisible ? 'scale(1)' : 'scale(0.95)'}`,\n };\n case 'bottom':\n return {\n ...baseStyles,\n top: rect.bottom + 8,\n left: centeredLeft(),\n transform: `translateX(-50%) ${isVisible ? 'scale(1)' : 'scale(0.95)'}`,\n };\n case 'left':\n return {\n ...baseStyles,\n top: rect.top + rect.height / 2,\n left: rect.left - 8,\n transform: `translate(-100%, -50%) ${isVisible ? 'scale(1)' : 'scale(0.95)'}`,\n };\n case 'right':\n return {\n ...baseStyles,\n top: rect.top + rect.height / 2,\n left: rect.right + 8,\n transform: `translateY(-50%) ${isVisible ? 'scale(1)' : 'scale(0.95)'}`,\n };\n default:\n return {\n ...baseStyles,\n top: rect.top - 8,\n left: centeredLeft(),\n transform: `translate(-50%, -100%) ${isVisible ? 'scale(1)' : 'scale(0.95)'}`,\n };\n }\n };\n\n // Only reference the tooltip id once the portal can exist — a dangling\n // aria-describedby (text set, never shown) confuses assistive tech.\n const describedBy = hasText && hasShown && describesTrigger ? tooltipId : undefined;\n useInsertionEffect(() => {\n if (hasText && hasShown) {\n ensureOverlayStyles();\n }\n }, [hasText, hasShown]);\n const renderedChildren =\n wrapChildren || !React.isValidElement(children) ? (\n <span aria-describedby={describedBy} style={fill ? FILL_STYLE : undefined}>\n {children}\n </span>\n ) : (\n React.cloneElement(children as React.ReactElement, {\n 'aria-describedby':\n [(children as React.ReactElement).props['aria-describedby'], describedBy]\n .filter(Boolean)\n .join(' ') || undefined,\n })\n );\n\n const Trigger = inline ? 'span' : triggerAs;\n\n // Create the tooltip content without its own shadow root\n return (\n <>\n <Trigger\n ref={triggerRef}\n className={`relative ${inline ? 'inline-flex max-w-full align-middle' : 'flex'} items-center`}\n style={triggerStyle}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleMouseLeave}\n onFocus={handleFocus}\n onBlur={handleBlur}\n >\n {renderedChildren}\n </Trigger>\n\n {hasText &&\n hasShown &&\n createPortal(\n <div\n ref={tooltipRef}\n id={tooltipId}\n role=\"tooltip\"\n // Nothing references it, and the sentence is already on screen and\n // described: left in the tree it is a second copy to read past.\n aria-hidden={describesTrigger ? undefined : true}\n {...overlayIdentity('tooltip')}\n style={getTooltipStyles()}\n >\n {/* min-w-0 + overflow-wrap let long values wrap within maxWidth. */}\n <span\n style={{\n minWidth: 0,\n overflowWrap: 'anywhere',\n }}\n >\n {typeof text === 'string' ? withBreakOpportunities(text) : text}\n </span>\n </div>,\n document.body,\n )}\n </>\n );\n};\n\nexport default Tooltip;\n"],"names":["WRAP_AFTER","withBreakOpportunities","value","chunk","i","jsxs","React","SHRINKABLE_STYLE","FILL_STYLE","VIEWPORT_MARGIN","Tooltip","text","children","delay","wrapChildren","triggerAs","inline","placement","isShrinkable","maxWidth","fill","shouldShow","describesTrigger","triggerStyle","isVisible","setIsVisible","useState","hasShown","setHasShown","timeoutRef","useRef","showFrameRef","cancelPendingShow","useCallback","scheduleVisible","tooltipRef","triggerRef","useTopLayer","tooltipId","useId","hasText","handleMouseEnter","handleMouseLeave","handleFocus","handleBlur","forceUpdate","useEffect","handleUpdate","getTooltipStyles","rect","baseStyles","centeredLeft","desired","width","_a","half","min","max","describedBy","useInsertionEffect","ensureOverlayStyles","renderedChildren","jsx","Fragment","createPortal","overlayIdentity"],"mappings":"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"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { jsx as
|
|
2
|
-
|
|
1
|
+
import { jsx as s } from "react/jsx-runtime";
|
|
2
|
+
import { useRef as h } from "react";
|
|
3
|
+
import g from "../Tooltip.js";
|
|
4
|
+
const b = (e) => {
|
|
3
5
|
switch (e) {
|
|
4
6
|
case "xsmall":
|
|
5
7
|
return "12px";
|
|
@@ -16,34 +18,55 @@ const d = (e) => {
|
|
|
16
18
|
default:
|
|
17
19
|
return "14px";
|
|
18
20
|
}
|
|
19
|
-
},
|
|
21
|
+
}, S = ({
|
|
20
22
|
bold: e = !1,
|
|
21
|
-
children:
|
|
22
|
-
size:
|
|
23
|
-
hasBorder:
|
|
24
|
-
variant:
|
|
23
|
+
children: o,
|
|
24
|
+
size: c = "small",
|
|
25
|
+
hasBorder: r = !1,
|
|
26
|
+
variant: d = "block",
|
|
25
27
|
// Deliberately NOT `inherit`: secondary prose would drop the chip to 3.65:1 in dark mode,
|
|
26
28
|
// under AA's 4.5:1. Pinning primary measures 18.5:1 light / 9.4:1 dark.
|
|
27
|
-
color:
|
|
28
|
-
backgroundColor:
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
color: p = "var(--malachite-text-primary)",
|
|
30
|
+
backgroundColor: x,
|
|
31
|
+
truncate: l = !1,
|
|
32
|
+
className: m = "",
|
|
33
|
+
style: f = {}
|
|
31
34
|
}) => {
|
|
32
|
-
const
|
|
35
|
+
const a = h(null), t = d === "inline", u = {
|
|
33
36
|
fontFamily: "var(--malachite-font-mono, monospace)",
|
|
34
|
-
fontSize:
|
|
37
|
+
fontSize: t ? "0.9em" : b(c),
|
|
35
38
|
fontWeight: e ? 600 : 400,
|
|
36
|
-
color:
|
|
37
|
-
backgroundColor:
|
|
38
|
-
borderRadius:
|
|
39
|
-
border:
|
|
40
|
-
padding:
|
|
39
|
+
color: p,
|
|
40
|
+
backgroundColor: r ? "transparent" : x || "var(--malachite-card-dimmed-background, #f5f5f5)",
|
|
41
|
+
borderRadius: t ? "4px" : "8px",
|
|
42
|
+
border: r ? "none" : "1px solid var(--malachite-card-border, #e8e8e8)",
|
|
43
|
+
padding: r ? "0px" : t ? "0 4px" : "4px 8px",
|
|
41
44
|
display: "inline",
|
|
42
|
-
...
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
...l ? {
|
|
46
|
+
display: "inline-block",
|
|
47
|
+
maxWidth: "100%",
|
|
48
|
+
overflow: "hidden",
|
|
49
|
+
textOverflow: "ellipsis",
|
|
50
|
+
whiteSpace: "nowrap",
|
|
51
|
+
lineHeight: 1.35
|
|
52
|
+
} : {},
|
|
53
|
+
...f
|
|
54
|
+
}, i = /* @__PURE__ */ s("code", { ref: a, className: m, style: u, children: o });
|
|
55
|
+
return l ? /* @__PURE__ */ s(
|
|
56
|
+
g,
|
|
57
|
+
{
|
|
58
|
+
text: o,
|
|
59
|
+
inline: !0,
|
|
60
|
+
isShrinkable: !0,
|
|
61
|
+
shouldShow: () => {
|
|
62
|
+
const n = a.current;
|
|
63
|
+
return !!n && n.scrollWidth > n.clientWidth;
|
|
64
|
+
},
|
|
65
|
+
children: i
|
|
66
|
+
}
|
|
67
|
+
) : i;
|
|
45
68
|
};
|
|
46
69
|
export {
|
|
47
|
-
|
|
70
|
+
S as default
|
|
48
71
|
};
|
|
49
72
|
//# sourceMappingURL=CodeText.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CodeText.js","sources":["../../../src/components/Typography/CodeText.tsx"],"sourcesContent":["import React from 'react';\nimport type { FontSize } from './Text';\n\nconst getFontSize = (size: FontSize): string => {\n switch (size) {\n case 'xsmall':\n return '12px';\n case 'small':\n return '14px';\n case 'medium':\n return '16px';\n case 'large':\n return '18px';\n case 'xlarge':\n return '20px';\n case 'xxlarge':\n return '24px';\n default:\n return '14px';\n }\n};\n\nexport type CodeTextVariant = 'block' | 'inline';\n\nexport interface CodeTextProps {\n bold?: boolean;\n size?: FontSize;\n hasBorder?: boolean;\n variant?: CodeTextVariant;\n color?: string;\n backgroundColor?: string;\n children: React.ReactNode;\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst CodeText: React.FC<CodeTextProps> = ({\n bold = false,\n children,\n size = 'small',\n hasBorder = false,\n variant = 'block',\n // Deliberately NOT `inherit`: secondary prose would drop the chip to 3.65:1 in dark mode,\n // under AA's 4.5:1. Pinning primary measures 18.5:1 light / 9.4:1 dark.\n color = 'var(--malachite-text-primary)',\n backgroundColor,\n className = '',\n style = {},\n}) => {\n const isInline = variant === 'inline';\n const codeStyles: React.CSSProperties = {\n fontFamily: 'var(--malachite-font-mono, monospace)',\n fontSize: isInline ? '0.9em' : getFontSize(size),\n fontWeight: bold ? 600 : 400,\n color,\n backgroundColor: hasBorder\n ? 'transparent'\n : backgroundColor || 'var(--malachite-card-dimmed-background, #f5f5f5)',\n borderRadius: isInline ? '4px' : '8px',\n border: hasBorder ? 'none' : '1px solid var(--malachite-card-border, #e8e8e8)',\n padding: hasBorder ? '0px' : isInline ? '0 4px' : '4px 8px',\n display: 'inline',\n ...style,\n };\n\n
|
|
1
|
+
{"version":3,"file":"CodeText.js","sources":["../../../src/components/Typography/CodeText.tsx"],"sourcesContent":["import React, { useRef } from 'react';\nimport Tooltip from '../Tooltip';\nimport type { FontSize } from './Text';\n\nconst getFontSize = (size: FontSize): string => {\n switch (size) {\n case 'xsmall':\n return '12px';\n case 'small':\n return '14px';\n case 'medium':\n return '16px';\n case 'large':\n return '18px';\n case 'xlarge':\n return '20px';\n case 'xxlarge':\n return '24px';\n default:\n return '14px';\n }\n};\n\nexport type CodeTextVariant = 'block' | 'inline';\n\nexport interface CodeTextProps {\n bold?: boolean;\n size?: FontSize;\n hasBorder?: boolean;\n variant?: CodeTextVariant;\n color?: string;\n backgroundColor?: string;\n children: React.ReactNode;\n truncate?: boolean;\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst CodeText: React.FC<CodeTextProps> = ({\n bold = false,\n children,\n size = 'small',\n hasBorder = false,\n variant = 'block',\n // Deliberately NOT `inherit`: secondary prose would drop the chip to 3.65:1 in dark mode,\n // under AA's 4.5:1. Pinning primary measures 18.5:1 light / 9.4:1 dark.\n color = 'var(--malachite-text-primary)',\n backgroundColor,\n truncate = false,\n className = '',\n style = {},\n}) => {\n const codeRef = useRef<HTMLElement>(null);\n const isInline = variant === 'inline';\n const codeStyles: React.CSSProperties = {\n fontFamily: 'var(--malachite-font-mono, monospace)',\n fontSize: isInline ? '0.9em' : getFontSize(size),\n fontWeight: bold ? 600 : 400,\n color,\n backgroundColor: hasBorder\n ? 'transparent'\n : backgroundColor || 'var(--malachite-card-dimmed-background, #f5f5f5)',\n borderRadius: isInline ? '4px' : '8px',\n border: hasBorder ? 'none' : '1px solid var(--malachite-card-border, #e8e8e8)',\n padding: hasBorder ? '0px' : isInline ? '0 4px' : '4px 8px',\n display: 'inline',\n ...(truncate\n ? {\n display: 'inline-block',\n maxWidth: '100%',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n lineHeight: 1.35,\n }\n : {}),\n ...style,\n };\n\n const code = (\n <code ref={codeRef} className={className} style={codeStyles}>\n {children}\n </code>\n );\n\n if (!truncate) {\n return code;\n }\n\n return (\n <Tooltip\n text={children}\n inline\n isShrinkable\n shouldShow={() => {\n const node = codeRef.current;\n return !!node && node.scrollWidth > node.clientWidth;\n }}\n >\n {code}\n </Tooltip>\n );\n};\n\nexport default CodeText;\n"],"names":["getFontSize","size","CodeText","bold","children","hasBorder","variant","color","backgroundColor","truncate","className","style","codeRef","useRef","isInline","codeStyles","code","jsx","Tooltip","node"],"mappings":";;;AAIA,MAAMA,IAAc,CAACC,MAA2B;AAC5C,UAAQA,GAAA;AAAA,IACJ,KAAK;AACD,aAAO;AAAA,IACX,KAAK;AACD,aAAO;AAAA,IACX,KAAK;AACD,aAAO;AAAA,IACX,KAAK;AACD,aAAO;AAAA,IACX,KAAK;AACD,aAAO;AAAA,IACX,KAAK;AACD,aAAO;AAAA,IACX;AACI,aAAO;AAAA,EAAA;AAEnB,GAiBMC,IAAoC,CAAC;AAAA,EACvC,MAAAC,IAAO;AAAA,EACP,UAAAC;AAAA,EACA,MAAAH,IAAO;AAAA,EACP,WAAAI,IAAY;AAAA,EACZ,SAAAC,IAAU;AAAA;AAAA;AAAA,EAGV,OAAAC,IAAQ;AAAA,EACR,iBAAAC;AAAA,EACA,UAAAC,IAAW;AAAA,EACX,WAAAC,IAAY;AAAA,EACZ,OAAAC,IAAQ,CAAA;AACZ,MAAM;AACF,QAAMC,IAAUC,EAAoB,IAAI,GAClCC,IAAWR,MAAY,UACvBS,IAAkC;AAAA,IACpC,YAAY;AAAA,IACZ,UAAUD,IAAW,UAAUd,EAAYC,CAAI;AAAA,IAC/C,YAAYE,IAAO,MAAM;AAAA,IACzB,OAAAI;AAAA,IACA,iBAAiBF,IACX,gBACAG,KAAmB;AAAA,IACzB,cAAcM,IAAW,QAAQ;AAAA,IACjC,QAAQT,IAAY,SAAS;AAAA,IAC7B,SAASA,IAAY,QAAQS,IAAW,UAAU;AAAA,IAClD,SAAS;AAAA,IACT,GAAIL,IACE;AAAA,MACI,SAAS;AAAA,MACT,UAAU;AAAA,MACV,UAAU;AAAA,MACV,cAAc;AAAA,MACd,YAAY;AAAA,MACZ,YAAY;AAAA,IAAA,IAEhB,CAAA;AAAA,IACN,GAAGE;AAAA,EAAA,GAGDK,sBACD,QAAA,EAAK,KAAKJ,GAAS,WAAAF,GAAsB,OAAOK,GAC5C,UAAAX,GACL;AAGJ,SAAKK,IAKD,gBAAAQ;AAAA,IAACC;AAAA,IAAA;AAAA,MACG,MAAMd;AAAA,MACN,QAAM;AAAA,MACN,cAAY;AAAA,MACZ,YAAY,MAAM;AACd,cAAMe,IAAOP,EAAQ;AACrB,eAAO,CAAC,CAACO,KAAQA,EAAK,cAAcA,EAAK;AAAA,MAC7C;AAAA,MAEC,UAAAH;AAAA,IAAA;AAAA,EAAA,IAbEA;AAgBf;"}
|
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
import { jsx as i } from "react/jsx-runtime";
|
|
2
|
-
import { useRef as
|
|
3
|
-
import
|
|
4
|
-
import { getFontSize as
|
|
5
|
-
const
|
|
2
|
+
import { useRef as u } from "react";
|
|
3
|
+
import h from "../Tooltip.js";
|
|
4
|
+
import { getFontSize as x, getTextColor as S } from "./textStyles.js";
|
|
5
|
+
const T = ({
|
|
6
6
|
children: n,
|
|
7
7
|
tooltip: t,
|
|
8
8
|
maxWidth: s = "100%",
|
|
9
9
|
color: p = "primary",
|
|
10
10
|
size: o,
|
|
11
11
|
tooltipWhenTruncated: c = !1,
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
tooltipPlacement: f,
|
|
13
|
+
className: a = "",
|
|
14
|
+
style: m = {}
|
|
14
15
|
}) => {
|
|
15
16
|
const d = {
|
|
16
|
-
color:
|
|
17
|
-
...o ? { fontSize:
|
|
17
|
+
color: S(p),
|
|
18
|
+
...o ? { fontSize: x(o) } : {},
|
|
18
19
|
maxWidth: s,
|
|
19
20
|
overflow: "hidden",
|
|
20
21
|
whiteSpace: "nowrap",
|
|
21
22
|
textOverflow: "ellipsis",
|
|
22
23
|
display: "inline-block",
|
|
23
24
|
verticalAlign: "top",
|
|
24
|
-
...
|
|
25
|
-
}, r =
|
|
25
|
+
...m
|
|
26
|
+
}, r = u(null), l = /* @__PURE__ */ i("span", { ref: r, className: a, style: d, children: n });
|
|
26
27
|
return t ? /* @__PURE__ */ i(
|
|
27
|
-
|
|
28
|
+
h,
|
|
28
29
|
{
|
|
29
30
|
text: t,
|
|
31
|
+
placement: f,
|
|
30
32
|
isShrinkable: !0,
|
|
31
33
|
shouldShow: c ? () => {
|
|
32
34
|
const e = r.current;
|
|
@@ -37,6 +39,6 @@ const g = ({
|
|
|
37
39
|
) : l;
|
|
38
40
|
};
|
|
39
41
|
export {
|
|
40
|
-
|
|
42
|
+
T as default
|
|
41
43
|
};
|
|
42
44
|
//# sourceMappingURL=EllipsisText.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EllipsisText.js","sources":["../../../src/components/Typography/EllipsisText.tsx"],"sourcesContent":["import React, { useRef } from 'react';\nimport Tooltip from '../Tooltip';\nimport type { TextColor } from './Text';\nimport type { FontSize } from './textStyles';\nimport { getFontSize, getTextColor } from './textStyles';\n\nexport interface EllipsisTextProps {\n children: React.ReactNode;\n tooltip?: React.ReactNode | string;\n /**\n * Defaults to filling the container. Pass a fixed width only when the text should\n * truncate before its container runs out of room (e.g. a column with a hard cap).\n */\n maxWidth?: string;\n color?: TextColor;\n size?: FontSize;\n /**\n * Show `tooltip` only when the text is actually cut off. Opt-in, because `tooltip` is any\n * node: a caller repeating the label wants this, and one supplying something the label does\n * not say — a full path, a timestamp — would lose it.\n */\n tooltipWhenTruncated?: boolean;\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst EllipsisText: React.FC<EllipsisTextProps> = ({\n children,\n tooltip,\n maxWidth = '100%',\n color = 'primary',\n size,\n tooltipWhenTruncated = false,\n className = '',\n style = {},\n}) => {\n const ellipsisStyles: React.CSSProperties = {\n color: getTextColor(color),\n ...(size ? { fontSize: getFontSize(size) } : {}),\n maxWidth,\n overflow: 'hidden',\n whiteSpace: 'nowrap',\n textOverflow: 'ellipsis',\n display: 'inline-block',\n verticalAlign: 'top',\n ...style,\n };\n\n const textRef = useRef<HTMLSpanElement>(null);\n\n const textElement = (\n <span ref={textRef} className={className} style={ellipsisStyles}>\n {children}\n </span>\n );\n\n if (tooltip) {\n // Measured on hover, not at mount: what fits depends on the container and on webfonts\n // that land after `DOMContentLoaded`, so a value read once is wrong for the rest of the\n // page's life.\n // Without `isShrinkable` the tooltip wrapper is floored at min-content and a percentage `maxWidth` never truncates.\n return (\n <Tooltip\n text={tooltip}\n isShrinkable\n shouldShow={\n tooltipWhenTruncated\n ? () => {\n const node = textRef.current;\n return !!node && node.scrollWidth > node.clientWidth;\n }\n : undefined\n }\n >\n {textElement}\n </Tooltip>\n );\n }\n\n return textElement;\n};\n\nexport default EllipsisText;\n"],"names":["EllipsisText","children","tooltip","maxWidth","color","size","tooltipWhenTruncated","className","style","ellipsisStyles","getTextColor","getFontSize","textRef","useRef","textElement","jsx","Tooltip","node"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"EllipsisText.js","sources":["../../../src/components/Typography/EllipsisText.tsx"],"sourcesContent":["import React, { useRef } from 'react';\nimport Tooltip from '../Tooltip';\nimport type { TextColor } from './Text';\nimport type { FontSize } from './textStyles';\nimport { getFontSize, getTextColor } from './textStyles';\n\nexport interface EllipsisTextProps {\n children: React.ReactNode;\n tooltip?: React.ReactNode | string;\n /**\n * Defaults to filling the container. Pass a fixed width only when the text should\n * truncate before its container runs out of room (e.g. a column with a hard cap).\n */\n maxWidth?: string;\n color?: TextColor;\n size?: FontSize;\n /**\n * Show `tooltip` only when the text is actually cut off. Opt-in, because `tooltip` is any\n * node: a caller repeating the label wants this, and one supplying something the label does\n * not say — a full path, a timestamp — would lose it.\n */\n tooltipWhenTruncated?: boolean;\n tooltipPlacement?: 'top' | 'bottom' | 'left' | 'right';\n className?: string;\n style?: React.CSSProperties;\n}\n\nconst EllipsisText: React.FC<EllipsisTextProps> = ({\n children,\n tooltip,\n maxWidth = '100%',\n color = 'primary',\n size,\n tooltipWhenTruncated = false,\n tooltipPlacement,\n className = '',\n style = {},\n}) => {\n const ellipsisStyles: React.CSSProperties = {\n color: getTextColor(color),\n ...(size ? { fontSize: getFontSize(size) } : {}),\n maxWidth,\n overflow: 'hidden',\n whiteSpace: 'nowrap',\n textOverflow: 'ellipsis',\n display: 'inline-block',\n verticalAlign: 'top',\n ...style,\n };\n\n const textRef = useRef<HTMLSpanElement>(null);\n\n const textElement = (\n <span ref={textRef} className={className} style={ellipsisStyles}>\n {children}\n </span>\n );\n\n if (tooltip) {\n // Measured on hover, not at mount: what fits depends on the container and on webfonts\n // that land after `DOMContentLoaded`, so a value read once is wrong for the rest of the\n // page's life.\n // Without `isShrinkable` the tooltip wrapper is floored at min-content and a percentage `maxWidth` never truncates.\n return (\n <Tooltip\n text={tooltip}\n placement={tooltipPlacement}\n isShrinkable\n shouldShow={\n tooltipWhenTruncated\n ? () => {\n const node = textRef.current;\n return !!node && node.scrollWidth > node.clientWidth;\n }\n : undefined\n }\n >\n {textElement}\n </Tooltip>\n );\n }\n\n return textElement;\n};\n\nexport default EllipsisText;\n"],"names":["EllipsisText","children","tooltip","maxWidth","color","size","tooltipWhenTruncated","tooltipPlacement","className","style","ellipsisStyles","getTextColor","getFontSize","textRef","useRef","textElement","jsx","Tooltip","node"],"mappings":";;;;AA2BA,MAAMA,IAA4C,CAAC;AAAA,EAC/C,UAAAC;AAAA,EACA,SAAAC;AAAA,EACA,UAAAC,IAAW;AAAA,EACX,OAAAC,IAAQ;AAAA,EACR,MAAAC;AAAA,EACA,sBAAAC,IAAuB;AAAA,EACvB,kBAAAC;AAAA,EACA,WAAAC,IAAY;AAAA,EACZ,OAAAC,IAAQ,CAAA;AACZ,MAAM;AACF,QAAMC,IAAsC;AAAA,IACxC,OAAOC,EAAaP,CAAK;AAAA,IACzB,GAAIC,IAAO,EAAE,UAAUO,EAAYP,CAAI,EAAA,IAAM,CAAA;AAAA,IAC7C,UAAAF;AAAA,IACA,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,SAAS;AAAA,IACT,eAAe;AAAA,IACf,GAAGM;AAAA,EAAA,GAGDI,IAAUC,EAAwB,IAAI,GAEtCC,sBACD,QAAA,EAAK,KAAKF,GAAS,WAAAL,GAAsB,OAAOE,GAC5C,UAAAT,GACL;AAGJ,SAAIC,IAMI,gBAAAc;AAAA,IAACC;AAAA,IAAA;AAAA,MACG,MAAMf;AAAA,MACN,WAAWK;AAAA,MACX,cAAY;AAAA,MACZ,YACID,IACM,MAAM;AACF,cAAMY,IAAOL,EAAQ;AACrB,eAAO,CAAC,CAACK,KAAQA,EAAK,cAAcA,EAAK;AAAA,MAC7C,IACA;AAAA,MAGT,UAAAH;AAAA,IAAA;AAAA,EAAA,IAKNA;AACX;"}
|