|
|
@ -38,6 +38,52 @@ const SHEET_MIN_SIZE = 0.6; |
|
|
const SHEET_MAX_SIZE = 1; |
|
|
const SHEET_MAX_SIZE = 1; |
|
|
const GESTURE_ENGAGE_PX = 10; |
|
|
const GESTURE_ENGAGE_PX = 10; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Character-based dynamic height algorithm for option cards in question sheets. |
|
|
|
|
|
* Calculates the required minimum height based on character count and word wrapping, |
|
|
|
|
|
* ensuring the card border completely encloses all lines of text without overflow or clipping. |
|
|
|
|
|
*/ |
|
|
|
|
|
export function estimateOptionLines(text: string, charsPerLine: number = 28): number { |
|
|
|
|
|
if (!text) return 1; |
|
|
|
|
|
const clean = text.trim(); |
|
|
|
|
|
if (clean.length === 0) return 1; |
|
|
|
|
|
const words = clean.split(/\s+/); |
|
|
|
|
|
let lines = 1; |
|
|
|
|
|
let currentLen = 0; |
|
|
|
|
|
for (const word of words) { |
|
|
|
|
|
if (currentLen + word.length > charsPerLine && currentLen > 0) { |
|
|
|
|
|
lines++; |
|
|
|
|
|
currentLen = word.length; |
|
|
|
|
|
} else { |
|
|
|
|
|
currentLen += (currentLen === 0 ? 0 : 1) + word.length; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
const charLines = Math.ceil(clean.length / charsPerLine); |
|
|
|
|
|
return Math.max(1, lines, charLines); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function calculateOptionMinHeight(label: string): number { |
|
|
|
|
|
if (!label) return 52; |
|
|
|
|
|
|
|
|
|
|
|
// If option has " - " separating title and description:
|
|
|
|
|
|
if (label.includes(" - ")) { |
|
|
|
|
|
const parts = label.split(" - "); |
|
|
|
|
|
const title = parts[0] || ""; |
|
|
|
|
|
const desc = parts.slice(1).join(" - ") || ""; |
|
|
|
|
|
const titleLines = estimateOptionLines(title, 26); |
|
|
|
|
|
const descLines = estimateOptionLines(desc, 30); |
|
|
|
|
|
// Vertical padding 24px + border 2px + title lines * 22px + gap 6px + desc lines * 19px + 4px breathing room
|
|
|
|
|
|
const total = 26 + (titleLines * 22) + 6 + (descLines * 19) + 4; |
|
|
|
|
|
return Math.max(52, Math.round(total)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const lines = estimateOptionLines(label, 28); |
|
|
|
|
|
// Base padding + border = 26px (py-3: 12px top + 12px bottom + 2px border)
|
|
|
|
|
|
// Each line is ~22px (15px font * 1.45 line-height) + 4px safety buffer
|
|
|
|
|
|
const total = 26 + (lines * 22) + 4; |
|
|
|
|
|
return Math.max(52, Math.round(total)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
export type QuestionSheetProps = { |
|
|
export type QuestionSheetProps = { |
|
|
question: QuestionField; |
|
|
question: QuestionField; |
|
|
disabled?: boolean; |
|
|
disabled?: boolean; |
|
|
@ -941,11 +987,14 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { |
|
|
!isSelected && |
|
|
!isSelected && |
|
|
Boolean(maxSelect && localSelectedList.length >= maxSelect); |
|
|
Boolean(maxSelect && localSelectedList.length >= maxSelect); |
|
|
|
|
|
|
|
|
|
|
|
const minHeightPx = calculateOptionMinHeight(option.label); |
|
|
|
|
|
|
|
|
return ( |
|
|
return ( |
|
|
<button |
|
|
<button |
|
|
key={option.id} |
|
|
key={option.id} |
|
|
type="button" |
|
|
type="button" |
|
|
disabled={isOptionDisabled} |
|
|
disabled={isOptionDisabled} |
|
|
|
|
|
aria-pressed={isMulti ? isSelected : undefined} |
|
|
onClick={() => { |
|
|
onClick={() => { |
|
|
if (isOptionDisabled) return; |
|
|
if (isOptionDisabled) return; |
|
|
if (isMulti) { |
|
|
if (isMulti) { |
|
|
@ -954,13 +1003,16 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { |
|
|
handleSelectSingle(option.id); |
|
|
handleSelectSingle(option.id); |
|
|
} |
|
|
} |
|
|
}} |
|
|
}} |
|
|
|
|
|
style={{ |
|
|
|
|
|
minHeight: `${minHeightPx}px`, |
|
|
|
|
|
}} |
|
|
className={[ |
|
|
className={[ |
|
|
"flex min-h-[52px] w-full items-start gap-3 rounded-[12px] border px-3.5 py-3 text-start transition-colors", |
|
|
|
|
|
|
|
|
"flex h-auto w-full shrink-0 items-start gap-3 rounded-[12px] border px-3.5 py-3 text-start transition-colors select-none box-border", |
|
|
isOptionDisabled |
|
|
isOptionDisabled |
|
|
? "opacity-35 cursor-not-allowed bg-[#F9FAFB] border-[#F2F4F7] text-[#98A2B3] pointer-events-none" |
|
|
|
|
|
|
|
|
? "opacity-35 cursor-not-allowed bg-[#F9FAFB] border-[#F2F4F7] text-[#98A2B3]" |
|
|
: isSelected |
|
|
: isSelected |
|
|
? "bg-[#FFF4F5] border-[#F0445B]/40 text-[#181818] cursor-pointer" |
|
|
|
|
|
: "bg-white hover:bg-[#F2F4F7] border-[#EAECF0] text-[#181818] cursor-pointer", |
|
|
|
|
|
|
|
|
? "bg-[#FFF4F5] border-[#F0445B]/40 text-[#181818] cursor-pointer active:scale-[0.995]" |
|
|
|
|
|
: "bg-white hover:bg-[#F2F4F7] border-[#EAECF0] text-[#181818] cursor-pointer active:scale-[0.995]", |
|
|
].join(" ")} |
|
|
].join(" ")} |
|
|
> |
|
|
> |
|
|
{/* Indicator Icon */} |
|
|
{/* Indicator Icon */} |
|
|
@ -1008,7 +1060,7 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
)} |
|
|
|
|
|
|
|
|
<span className="text-[15px] leading-[1.45] flex-1 text-start break-words"> |
|
|
|
|
|
|
|
|
<div className="flex-1 min-w-0 text-start"> |
|
|
{option.label.includes(" - ") ? ( |
|
|
{option.label.includes(" - ") ? ( |
|
|
(() => { |
|
|
(() => { |
|
|
const parts = option.label.split(" - "); |
|
|
const parts = option.label.split(" - "); |
|
|
@ -1018,7 +1070,7 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { |
|
|
<span className="flex flex-col gap-1 text-start"> |
|
|
<span className="flex flex-col gap-1 text-start"> |
|
|
<span |
|
|
<span |
|
|
className={[ |
|
|
className={[ |
|
|
"font-bold", |
|
|
|
|
|
|
|
|
"font-bold text-[15px] leading-[1.45] break-words [overflow-wrap:anywhere]", |
|
|
isOptionDisabled |
|
|
isOptionDisabled |
|
|
? "text-[#98A2B3]" |
|
|
? "text-[#98A2B3]" |
|
|
: "text-[#181818]", |
|
|
: "text-[#181818]", |
|
|
@ -1040,18 +1092,19 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { |
|
|
})() |
|
|
})() |
|
|
) : ( |
|
|
) : ( |
|
|
<span |
|
|
<span |
|
|
className={ |
|
|
|
|
|
|
|
|
className={[ |
|
|
|
|
|
"text-[15px] leading-[1.45] block break-words [overflow-wrap:anywhere]", |
|
|
isOptionDisabled |
|
|
isOptionDisabled |
|
|
? "font-normal text-[#98A2B3] block" |
|
|
|
|
|
|
|
|
? "font-normal text-[#98A2B3]" |
|
|
: isSelected |
|
|
: isSelected |
|
|
? "font-bold text-[#181818] block" |
|
|
|
|
|
: "font-semibold text-[#344054] block" |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
? "font-bold text-[#181818]" |
|
|
|
|
|
: "font-semibold text-[#344054]", |
|
|
|
|
|
].join(" ")} |
|
|
> |
|
|
> |
|
|
{option.label} |
|
|
{option.label} |
|
|
</span> |
|
|
</span> |
|
|
)} |
|
|
)} |
|
|
</span> |
|
|
|
|
|
|
|
|
</div> |
|
|
</button> |
|
|
</button> |
|
|
); |
|
|
); |
|
|
}) |
|
|
}) |
|
|
|