@j2-health/design-system 0.6.10 → 0.6.11

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.
@@ -69,7 +69,7 @@ var SwatchGrid = ({ colors, value, defaultValue, label, onPick, gridRef, size })
69
69
  ref: gridRef,
70
70
  role: "radiogroup",
71
71
  "aria-label": `Color for ${label}`,
72
- className: "grid grid-cols-8 gap-1",
72
+ className: "grid w-fit grid-cols-8 gap-1",
73
73
  onKeyDown,
74
74
  children: colors.map((c, index) => {
75
75
  const selected = c.value === value;
@@ -1 +1 @@
1
- {"version":3,"file":"SwatchPicker.cjs","names":["useRef","useState","CheckIcon","cx","Swatch","Button","Popover"],"sources":["../../../../src/components/swatchPicker/SwatchPicker.tsx"],"sourcesContent":["import {\n type KeyboardEvent,\n type ReactNode,\n type RefObject,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from 'react'\nimport { CheckIcon } from '@phosphor-icons/react'\nimport cx from 'classnames'\nimport { Button } from '../button'\nimport { Popover, type PopoverProps } from '../popover'\nimport { Swatch } from '../swatch'\n\nexport type SwatchPickerColor = {\n /** What `onChange` reports: a token name, a hex, any id. */\n value: string\n /** Any CSS color. Defaults to `value`. */\n color?: string\n /** The swatch's accessible name. Defaults to `value`. */\n label?: string\n}\n\nexport type SwatchPickerProps = {\n /** The colors offered, in order. */\n colors: SwatchPickerColor[]\n /** The current color's `value`. */\n value: string\n /** What is being colored, for the grid's name: \"Color for {label}\". */\n label: string\n onChange: (value: string) => void\n /** The starting color. \"Reset to default\" shows once `value` differs from it. */\n defaultValue?: string\n /**\n * - `'popover'`: the grid opens from `children` and closes on a pick.\n * - `'inline'`: the grid in place, with no control or popover, and smaller\n * swatches (20px, against 24px in the popover).\n */\n variant?: 'popover' | 'inline'\n /** The control that opens the popover. Not used inline. */\n children?: ReactNode\n /** The popover's placement. */\n placement?: PopoverProps['placement']\n /** Where the popover renders. Defaults to the enclosing modal, or the page when there is none. */\n getPopupContainer?: PopoverProps['getPopupContainer']\n /**\n * Called when the popover opens or closes, e.g. to keep a panel around it\n * open meanwhile. Not used inline.\n */\n onOpenChange?: (open: boolean) => void\n}\n\nconst COLUMNS = 8\n\n// The maps picker's corners (2px); Swatch's default is 4px.\nconst RADIUS = 2\n\n// Centers the check on its swatch.\nconst CENTERED = {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n} as const\n\n// Arrow keys move through the grid; each is the step in swatches.\nconst STEPS: Record<string, number> = {\n ArrowRight: 1,\n ArrowLeft: -1,\n ArrowDown: COLUMNS,\n ArrowUp: -COLUMNS,\n}\n\n// Inside a modal the popover renders in the modal's container. A popover on\n// the page body can't take focus, since the modal pulls it back, and the\n// modal's outer box ignores the pointer, so only its container takes clicks.\nconst modalOrPage = (trigger: HTMLElement) =>\n trigger.closest<HTMLElement>('.ant-modal-container') ?? document.body\n\n// The first three channels of a painted color, 0 to 255. Browsers report\n// `rgb(…)` for most colors and `color(srgb …)`, 0 to 1, for color-mix results.\nconst channels = (painted: string) => {\n const numbers = painted.match(/[\\d.]+/g)?.map(Number)\n if (!numbers || numbers.length < 3) return null\n const scale = painted.startsWith('color(') ? 255 : 1\n return numbers.slice(0, 3).map((n) => n * scale)\n}\n\n// Whether a swatch needs a light check mark: Rec. 601 luma, which is enough\n// for a light-or-dark decision.\nconst isDark = (painted: string) => {\n const rgb = channels(painted)\n return rgb ? (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000 < 140 : true\n}\n\n// Colors may be CSS variables, so the check reads its swatch as painted.\nconst Check = () => {\n const ref = useRef<HTMLSpanElement>(null)\n const [dark, setDark] = useState(true)\n useLayoutEffect(() => {\n const swatch = ref.current?.parentElement\n if (swatch) setDark(isDark(getComputedStyle(swatch).backgroundColor))\n }, [])\n return (\n <span ref={ref} className=\"flex\">\n <CheckIcon\n size={12}\n weight=\"bold\"\n color=\"currentColor\"\n className={cx('block', dark ? 'text-white' : 'text-j2-text')}\n />\n </span>\n )\n}\n\n// The grid and its Reset link. Focus rests on one swatch at a time (roving\n// tabindex), so the grid is one Tab stop and the arrow keys move within it;\n// Enter or Space picks. It starts on the current color, or the first when\n// that isn't in the grid.\nconst SwatchGrid = ({\n colors,\n value,\n defaultValue,\n label,\n onPick,\n gridRef,\n size,\n}: {\n colors: SwatchPickerColor[]\n value: string\n defaultValue?: string\n label: string\n onPick: (value: string) => void\n gridRef?: RefObject<HTMLDivElement | null>\n /** Swatch side in px. */\n size: number\n}) => {\n const [focusIndex, setFocusIndex] = useState(() =>\n Math.max(\n 0,\n colors.findIndex((c) => c.value === value)\n )\n )\n const swatches = useRef<(HTMLButtonElement | null)[]>([])\n\n const onKeyDown = (event: KeyboardEvent) => {\n const step = STEPS[event.key]\n if (step === undefined) return\n event.preventDefault()\n const next = focusIndex + step\n if (next < 0 || next >= colors.length) return\n setFocusIndex(next)\n swatches.current[next]?.focus()\n }\n\n return (\n <div className=\"flex flex-col gap-2\">\n <div\n ref={gridRef}\n role=\"radiogroup\"\n aria-label={`Color for ${label}`}\n className=\"grid grid-cols-8 gap-1\"\n onKeyDown={onKeyDown}\n >\n {colors.map((c, index) => {\n const selected = c.value === value\n return (\n <button\n key={c.value}\n ref={(node) => {\n swatches.current[index] = node\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n aria-label={c.label ?? c.value}\n tabIndex={index === focusIndex ? 0 : -1}\n onClick={() => {\n // Inline the grid stays, so arrow keys go on from the pick.\n setFocusIndex(index)\n onPick(c.value)\n }}\n className=\"flex cursor-pointer rounded-j2-xs border-0 bg-transparent p-0\"\n >\n <Swatch\n color={c.color ?? c.value}\n size={size}\n radius={RADIUS}\n style={CENTERED}\n >\n {selected && <Check />}\n </Swatch>\n </button>\n )\n })}\n </div>\n {/* Only offered when it would change something. */}\n {defaultValue !== undefined && value !== defaultValue && (\n <Button\n type=\"link\"\n size=\"small\"\n className=\"self-start !px-0\"\n onClick={() => onPick(defaultValue)}\n >\n Reset to default\n </Button>\n )}\n </div>\n )\n}\n\n/**\n * Pick one color from a fixed set, in a popover or inline. A grid rather than\n * a free color input, so two items can't end up on colors that are hard to\n * tell apart. The popover opens on the current color and closes on a pick.\n */\nexport const SwatchPicker = ({\n colors,\n value,\n label,\n onChange,\n defaultValue,\n variant = 'popover',\n children,\n placement = 'rightTop',\n getPopupContainer = modalOrPage,\n onOpenChange,\n}: SwatchPickerProps) => {\n const [open, setOpenState] = useState(false)\n const setOpen = (next: boolean) => {\n setOpenState(next)\n onOpenChange?.(next)\n }\n const gridRef = useRef<HTMLDivElement>(null)\n const triggerRef = useRef<HTMLSpanElement>(null)\n\n // Focus moves into the grid on open, onto the swatch that is its Tab stop:\n // a frame later, once the popover is in the page. (afterOpenChange waits\n // for the open animation.)\n useEffect(() => {\n if (!open) return\n const frame = requestAnimationFrame(() =>\n gridRef.current?.querySelector<HTMLElement>('[tabindex=\"0\"]')?.focus()\n )\n return () => cancelAnimationFrame(frame)\n }, [open])\n\n // A pick or Escape closes it and returns focus to the control that opened it.\n const close = () => {\n setOpen(false)\n triggerRef.current\n ?.querySelector<HTMLElement>('button, [tabindex]')\n ?.focus()\n }\n\n if (variant === 'inline') {\n return (\n <SwatchGrid\n colors={colors}\n value={value}\n defaultValue={defaultValue}\n label={label}\n onPick={onChange}\n size={20}\n />\n )\n }\n\n return (\n <Popover\n trigger=\"click\"\n placement={placement}\n open={open}\n onOpenChange={setOpen}\n getPopupContainer={getPopupContainer}\n destroyOnHidden\n paddingSize=\"xs\"\n content={\n <div\n onKeyDown={(event) => {\n // Only the picker closes, not a modal around it.\n if (event.key !== 'Escape') return\n event.stopPropagation()\n close()\n }}\n >\n <SwatchGrid\n colors={colors}\n value={value}\n defaultValue={defaultValue}\n label={label}\n gridRef={gridRef}\n size={24}\n onPick={(next) => {\n onChange(next)\n close()\n }}\n />\n </div>\n }\n >\n <span ref={triggerRef} className=\"inline-flex\">\n {children}\n </span>\n </Popover>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SwatchPicker.cjs","names":["useRef","useState","CheckIcon","cx","Swatch","Button","Popover"],"sources":["../../../../src/components/swatchPicker/SwatchPicker.tsx"],"sourcesContent":["import {\n type KeyboardEvent,\n type ReactNode,\n type RefObject,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from 'react'\nimport { CheckIcon } from '@phosphor-icons/react'\nimport cx from 'classnames'\nimport { Button } from '../button'\nimport { Popover, type PopoverProps } from '../popover'\nimport { Swatch } from '../swatch'\n\nexport type SwatchPickerColor = {\n /** What `onChange` reports: a token name, a hex, any id. */\n value: string\n /** Any CSS color. Defaults to `value`. */\n color?: string\n /** The swatch's accessible name. Defaults to `value`. */\n label?: string\n}\n\nexport type SwatchPickerProps = {\n /** The colors offered, in order. */\n colors: SwatchPickerColor[]\n /** The current color's `value`. */\n value: string\n /** What is being colored, for the grid's name: \"Color for {label}\". */\n label: string\n onChange: (value: string) => void\n /** The starting color. \"Reset to default\" shows once `value` differs from it. */\n defaultValue?: string\n /**\n * - `'popover'`: the grid opens from `children` and closes on a pick.\n * - `'inline'`: the grid in place, with no control or popover, and smaller\n * swatches (20px, against 24px in the popover).\n */\n variant?: 'popover' | 'inline'\n /** The control that opens the popover. Not used inline. */\n children?: ReactNode\n /** The popover's placement. */\n placement?: PopoverProps['placement']\n /** Where the popover renders. Defaults to the enclosing modal, or the page when there is none. */\n getPopupContainer?: PopoverProps['getPopupContainer']\n /**\n * Called when the popover opens or closes, e.g. to keep a panel around it\n * open meanwhile. Not used inline.\n */\n onOpenChange?: (open: boolean) => void\n}\n\nconst COLUMNS = 8\n\n// The maps picker's corners (2px); Swatch's default is 4px.\nconst RADIUS = 2\n\n// Centers the check on its swatch.\nconst CENTERED = {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n} as const\n\n// Arrow keys move through the grid; each is the step in swatches.\nconst STEPS: Record<string, number> = {\n ArrowRight: 1,\n ArrowLeft: -1,\n ArrowDown: COLUMNS,\n ArrowUp: -COLUMNS,\n}\n\n// Inside a modal the popover renders in the modal's container. A popover on\n// the page body can't take focus, since the modal pulls it back, and the\n// modal's outer box ignores the pointer, so only its container takes clicks.\nconst modalOrPage = (trigger: HTMLElement) =>\n trigger.closest<HTMLElement>('.ant-modal-container') ?? document.body\n\n// The first three channels of a painted color, 0 to 255. Browsers report\n// `rgb(…)` for most colors and `color(srgb …)`, 0 to 1, for color-mix results.\nconst channels = (painted: string) => {\n const numbers = painted.match(/[\\d.]+/g)?.map(Number)\n if (!numbers || numbers.length < 3) return null\n const scale = painted.startsWith('color(') ? 255 : 1\n return numbers.slice(0, 3).map((n) => n * scale)\n}\n\n// Whether a swatch needs a light check mark: Rec. 601 luma, which is enough\n// for a light-or-dark decision.\nconst isDark = (painted: string) => {\n const rgb = channels(painted)\n return rgb ? (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000 < 140 : true\n}\n\n// Colors may be CSS variables, so the check reads its swatch as painted.\nconst Check = () => {\n const ref = useRef<HTMLSpanElement>(null)\n const [dark, setDark] = useState(true)\n useLayoutEffect(() => {\n const swatch = ref.current?.parentElement\n if (swatch) setDark(isDark(getComputedStyle(swatch).backgroundColor))\n }, [])\n return (\n <span ref={ref} className=\"flex\">\n <CheckIcon\n size={12}\n weight=\"bold\"\n color=\"currentColor\"\n className={cx('block', dark ? 'text-white' : 'text-j2-text')}\n />\n </span>\n )\n}\n\n// The grid and its Reset link. Focus rests on one swatch at a time (roving\n// tabindex), so the grid is one Tab stop and the arrow keys move within it;\n// Enter or Space picks. It starts on the current color, or the first when\n// that isn't in the grid.\nconst SwatchGrid = ({\n colors,\n value,\n defaultValue,\n label,\n onPick,\n gridRef,\n size,\n}: {\n colors: SwatchPickerColor[]\n value: string\n defaultValue?: string\n label: string\n onPick: (value: string) => void\n gridRef?: RefObject<HTMLDivElement | null>\n /** Swatch side in px. */\n size: number\n}) => {\n const [focusIndex, setFocusIndex] = useState(() =>\n Math.max(\n 0,\n colors.findIndex((c) => c.value === value)\n )\n )\n const swatches = useRef<(HTMLButtonElement | null)[]>([])\n\n const onKeyDown = (event: KeyboardEvent) => {\n const step = STEPS[event.key]\n if (step === undefined) return\n event.preventDefault()\n const next = focusIndex + step\n if (next < 0 || next >= colors.length) return\n setFocusIndex(next)\n swatches.current[next]?.focus()\n }\n\n return (\n <div className=\"flex flex-col gap-2\">\n <div\n ref={gridRef}\n role=\"radiogroup\"\n aria-label={`Color for ${label}`}\n // Sized to its swatches: in a wider box the columns would stretch,\n // spreading the swatches and each button past its square.\n className=\"grid w-fit grid-cols-8 gap-1\"\n onKeyDown={onKeyDown}\n >\n {colors.map((c, index) => {\n const selected = c.value === value\n return (\n <button\n key={c.value}\n ref={(node) => {\n swatches.current[index] = node\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n aria-label={c.label ?? c.value}\n tabIndex={index === focusIndex ? 0 : -1}\n onClick={() => {\n // Inline the grid stays, so arrow keys go on from the pick.\n setFocusIndex(index)\n onPick(c.value)\n }}\n className=\"flex cursor-pointer rounded-j2-xs border-0 bg-transparent p-0\"\n >\n <Swatch\n color={c.color ?? c.value}\n size={size}\n radius={RADIUS}\n style={CENTERED}\n >\n {selected && <Check />}\n </Swatch>\n </button>\n )\n })}\n </div>\n {/* Only offered when it would change something. */}\n {defaultValue !== undefined && value !== defaultValue && (\n <Button\n type=\"link\"\n size=\"small\"\n className=\"self-start !px-0\"\n onClick={() => onPick(defaultValue)}\n >\n Reset to default\n </Button>\n )}\n </div>\n )\n}\n\n/**\n * Pick one color from a fixed set, in a popover or inline. A grid rather than\n * a free color input, so two items can't end up on colors that are hard to\n * tell apart. The popover opens on the current color and closes on a pick.\n */\nexport const SwatchPicker = ({\n colors,\n value,\n label,\n onChange,\n defaultValue,\n variant = 'popover',\n children,\n placement = 'rightTop',\n getPopupContainer = modalOrPage,\n onOpenChange,\n}: SwatchPickerProps) => {\n const [open, setOpenState] = useState(false)\n const setOpen = (next: boolean) => {\n setOpenState(next)\n onOpenChange?.(next)\n }\n const gridRef = useRef<HTMLDivElement>(null)\n const triggerRef = useRef<HTMLSpanElement>(null)\n\n // Focus moves into the grid on open, onto the swatch that is its Tab stop:\n // a frame later, once the popover is in the page. (afterOpenChange waits\n // for the open animation.)\n useEffect(() => {\n if (!open) return\n const frame = requestAnimationFrame(() =>\n gridRef.current?.querySelector<HTMLElement>('[tabindex=\"0\"]')?.focus()\n )\n return () => cancelAnimationFrame(frame)\n }, [open])\n\n // A pick or Escape closes it and returns focus to the control that opened it.\n const close = () => {\n setOpen(false)\n triggerRef.current\n ?.querySelector<HTMLElement>('button, [tabindex]')\n ?.focus()\n }\n\n if (variant === 'inline') {\n return (\n <SwatchGrid\n colors={colors}\n value={value}\n defaultValue={defaultValue}\n label={label}\n onPick={onChange}\n size={20}\n />\n )\n }\n\n return (\n <Popover\n trigger=\"click\"\n placement={placement}\n open={open}\n onOpenChange={setOpen}\n getPopupContainer={getPopupContainer}\n destroyOnHidden\n paddingSize=\"xs\"\n content={\n <div\n onKeyDown={(event) => {\n // Only the picker closes, not a modal around it.\n if (event.key !== 'Escape') return\n event.stopPropagation()\n close()\n }}\n >\n <SwatchGrid\n colors={colors}\n value={value}\n defaultValue={defaultValue}\n label={label}\n gridRef={gridRef}\n size={24}\n onPick={(next) => {\n onChange(next)\n close()\n }}\n />\n </div>\n }\n >\n <span ref={triggerRef} className=\"inline-flex\">\n {children}\n </span>\n </Popover>\n )\n}\n"],"mappings":"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"}
@@ -66,7 +66,7 @@ var SwatchGrid = ({ colors, value, defaultValue, label, onPick, gridRef, size })
66
66
  ref: gridRef,
67
67
  role: "radiogroup",
68
68
  "aria-label": `Color for ${label}`,
69
- className: "grid grid-cols-8 gap-1",
69
+ className: "grid w-fit grid-cols-8 gap-1",
70
70
  onKeyDown,
71
71
  children: colors.map((c, index) => {
72
72
  const selected = c.value === value;
@@ -1 +1 @@
1
- {"version":3,"file":"SwatchPicker.js","names":[],"sources":["../../../../src/components/swatchPicker/SwatchPicker.tsx"],"sourcesContent":["import {\n type KeyboardEvent,\n type ReactNode,\n type RefObject,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from 'react'\nimport { CheckIcon } from '@phosphor-icons/react'\nimport cx from 'classnames'\nimport { Button } from '../button'\nimport { Popover, type PopoverProps } from '../popover'\nimport { Swatch } from '../swatch'\n\nexport type SwatchPickerColor = {\n /** What `onChange` reports: a token name, a hex, any id. */\n value: string\n /** Any CSS color. Defaults to `value`. */\n color?: string\n /** The swatch's accessible name. Defaults to `value`. */\n label?: string\n}\n\nexport type SwatchPickerProps = {\n /** The colors offered, in order. */\n colors: SwatchPickerColor[]\n /** The current color's `value`. */\n value: string\n /** What is being colored, for the grid's name: \"Color for {label}\". */\n label: string\n onChange: (value: string) => void\n /** The starting color. \"Reset to default\" shows once `value` differs from it. */\n defaultValue?: string\n /**\n * - `'popover'`: the grid opens from `children` and closes on a pick.\n * - `'inline'`: the grid in place, with no control or popover, and smaller\n * swatches (20px, against 24px in the popover).\n */\n variant?: 'popover' | 'inline'\n /** The control that opens the popover. Not used inline. */\n children?: ReactNode\n /** The popover's placement. */\n placement?: PopoverProps['placement']\n /** Where the popover renders. Defaults to the enclosing modal, or the page when there is none. */\n getPopupContainer?: PopoverProps['getPopupContainer']\n /**\n * Called when the popover opens or closes, e.g. to keep a panel around it\n * open meanwhile. Not used inline.\n */\n onOpenChange?: (open: boolean) => void\n}\n\nconst COLUMNS = 8\n\n// The maps picker's corners (2px); Swatch's default is 4px.\nconst RADIUS = 2\n\n// Centers the check on its swatch.\nconst CENTERED = {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n} as const\n\n// Arrow keys move through the grid; each is the step in swatches.\nconst STEPS: Record<string, number> = {\n ArrowRight: 1,\n ArrowLeft: -1,\n ArrowDown: COLUMNS,\n ArrowUp: -COLUMNS,\n}\n\n// Inside a modal the popover renders in the modal's container. A popover on\n// the page body can't take focus, since the modal pulls it back, and the\n// modal's outer box ignores the pointer, so only its container takes clicks.\nconst modalOrPage = (trigger: HTMLElement) =>\n trigger.closest<HTMLElement>('.ant-modal-container') ?? document.body\n\n// The first three channels of a painted color, 0 to 255. Browsers report\n// `rgb(…)` for most colors and `color(srgb …)`, 0 to 1, for color-mix results.\nconst channels = (painted: string) => {\n const numbers = painted.match(/[\\d.]+/g)?.map(Number)\n if (!numbers || numbers.length < 3) return null\n const scale = painted.startsWith('color(') ? 255 : 1\n return numbers.slice(0, 3).map((n) => n * scale)\n}\n\n// Whether a swatch needs a light check mark: Rec. 601 luma, which is enough\n// for a light-or-dark decision.\nconst isDark = (painted: string) => {\n const rgb = channels(painted)\n return rgb ? (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000 < 140 : true\n}\n\n// Colors may be CSS variables, so the check reads its swatch as painted.\nconst Check = () => {\n const ref = useRef<HTMLSpanElement>(null)\n const [dark, setDark] = useState(true)\n useLayoutEffect(() => {\n const swatch = ref.current?.parentElement\n if (swatch) setDark(isDark(getComputedStyle(swatch).backgroundColor))\n }, [])\n return (\n <span ref={ref} className=\"flex\">\n <CheckIcon\n size={12}\n weight=\"bold\"\n color=\"currentColor\"\n className={cx('block', dark ? 'text-white' : 'text-j2-text')}\n />\n </span>\n )\n}\n\n// The grid and its Reset link. Focus rests on one swatch at a time (roving\n// tabindex), so the grid is one Tab stop and the arrow keys move within it;\n// Enter or Space picks. It starts on the current color, or the first when\n// that isn't in the grid.\nconst SwatchGrid = ({\n colors,\n value,\n defaultValue,\n label,\n onPick,\n gridRef,\n size,\n}: {\n colors: SwatchPickerColor[]\n value: string\n defaultValue?: string\n label: string\n onPick: (value: string) => void\n gridRef?: RefObject<HTMLDivElement | null>\n /** Swatch side in px. */\n size: number\n}) => {\n const [focusIndex, setFocusIndex] = useState(() =>\n Math.max(\n 0,\n colors.findIndex((c) => c.value === value)\n )\n )\n const swatches = useRef<(HTMLButtonElement | null)[]>([])\n\n const onKeyDown = (event: KeyboardEvent) => {\n const step = STEPS[event.key]\n if (step === undefined) return\n event.preventDefault()\n const next = focusIndex + step\n if (next < 0 || next >= colors.length) return\n setFocusIndex(next)\n swatches.current[next]?.focus()\n }\n\n return (\n <div className=\"flex flex-col gap-2\">\n <div\n ref={gridRef}\n role=\"radiogroup\"\n aria-label={`Color for ${label}`}\n className=\"grid grid-cols-8 gap-1\"\n onKeyDown={onKeyDown}\n >\n {colors.map((c, index) => {\n const selected = c.value === value\n return (\n <button\n key={c.value}\n ref={(node) => {\n swatches.current[index] = node\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n aria-label={c.label ?? c.value}\n tabIndex={index === focusIndex ? 0 : -1}\n onClick={() => {\n // Inline the grid stays, so arrow keys go on from the pick.\n setFocusIndex(index)\n onPick(c.value)\n }}\n className=\"flex cursor-pointer rounded-j2-xs border-0 bg-transparent p-0\"\n >\n <Swatch\n color={c.color ?? c.value}\n size={size}\n radius={RADIUS}\n style={CENTERED}\n >\n {selected && <Check />}\n </Swatch>\n </button>\n )\n })}\n </div>\n {/* Only offered when it would change something. */}\n {defaultValue !== undefined && value !== defaultValue && (\n <Button\n type=\"link\"\n size=\"small\"\n className=\"self-start !px-0\"\n onClick={() => onPick(defaultValue)}\n >\n Reset to default\n </Button>\n )}\n </div>\n )\n}\n\n/**\n * Pick one color from a fixed set, in a popover or inline. A grid rather than\n * a free color input, so two items can't end up on colors that are hard to\n * tell apart. The popover opens on the current color and closes on a pick.\n */\nexport const SwatchPicker = ({\n colors,\n value,\n label,\n onChange,\n defaultValue,\n variant = 'popover',\n children,\n placement = 'rightTop',\n getPopupContainer = modalOrPage,\n onOpenChange,\n}: SwatchPickerProps) => {\n const [open, setOpenState] = useState(false)\n const setOpen = (next: boolean) => {\n setOpenState(next)\n onOpenChange?.(next)\n }\n const gridRef = useRef<HTMLDivElement>(null)\n const triggerRef = useRef<HTMLSpanElement>(null)\n\n // Focus moves into the grid on open, onto the swatch that is its Tab stop:\n // a frame later, once the popover is in the page. (afterOpenChange waits\n // for the open animation.)\n useEffect(() => {\n if (!open) return\n const frame = requestAnimationFrame(() =>\n gridRef.current?.querySelector<HTMLElement>('[tabindex=\"0\"]')?.focus()\n )\n return () => cancelAnimationFrame(frame)\n }, [open])\n\n // A pick or Escape closes it and returns focus to the control that opened it.\n const close = () => {\n setOpen(false)\n triggerRef.current\n ?.querySelector<HTMLElement>('button, [tabindex]')\n ?.focus()\n }\n\n if (variant === 'inline') {\n return (\n <SwatchGrid\n colors={colors}\n value={value}\n defaultValue={defaultValue}\n label={label}\n onPick={onChange}\n size={20}\n />\n )\n }\n\n return (\n <Popover\n trigger=\"click\"\n placement={placement}\n open={open}\n onOpenChange={setOpen}\n getPopupContainer={getPopupContainer}\n destroyOnHidden\n paddingSize=\"xs\"\n content={\n <div\n onKeyDown={(event) => {\n // Only the picker closes, not a modal around it.\n if (event.key !== 'Escape') return\n event.stopPropagation()\n close()\n }}\n >\n <SwatchGrid\n colors={colors}\n value={value}\n defaultValue={defaultValue}\n label={label}\n gridRef={gridRef}\n size={24}\n onPick={(next) => {\n onChange(next)\n close()\n }}\n />\n </div>\n }\n >\n <span ref={triggerRef} className=\"inline-flex\">\n {children}\n </span>\n </Popover>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SwatchPicker.js","names":[],"sources":["../../../../src/components/swatchPicker/SwatchPicker.tsx"],"sourcesContent":["import {\n type KeyboardEvent,\n type ReactNode,\n type RefObject,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from 'react'\nimport { CheckIcon } from '@phosphor-icons/react'\nimport cx from 'classnames'\nimport { Button } from '../button'\nimport { Popover, type PopoverProps } from '../popover'\nimport { Swatch } from '../swatch'\n\nexport type SwatchPickerColor = {\n /** What `onChange` reports: a token name, a hex, any id. */\n value: string\n /** Any CSS color. Defaults to `value`. */\n color?: string\n /** The swatch's accessible name. Defaults to `value`. */\n label?: string\n}\n\nexport type SwatchPickerProps = {\n /** The colors offered, in order. */\n colors: SwatchPickerColor[]\n /** The current color's `value`. */\n value: string\n /** What is being colored, for the grid's name: \"Color for {label}\". */\n label: string\n onChange: (value: string) => void\n /** The starting color. \"Reset to default\" shows once `value` differs from it. */\n defaultValue?: string\n /**\n * - `'popover'`: the grid opens from `children` and closes on a pick.\n * - `'inline'`: the grid in place, with no control or popover, and smaller\n * swatches (20px, against 24px in the popover).\n */\n variant?: 'popover' | 'inline'\n /** The control that opens the popover. Not used inline. */\n children?: ReactNode\n /** The popover's placement. */\n placement?: PopoverProps['placement']\n /** Where the popover renders. Defaults to the enclosing modal, or the page when there is none. */\n getPopupContainer?: PopoverProps['getPopupContainer']\n /**\n * Called when the popover opens or closes, e.g. to keep a panel around it\n * open meanwhile. Not used inline.\n */\n onOpenChange?: (open: boolean) => void\n}\n\nconst COLUMNS = 8\n\n// The maps picker's corners (2px); Swatch's default is 4px.\nconst RADIUS = 2\n\n// Centers the check on its swatch.\nconst CENTERED = {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n} as const\n\n// Arrow keys move through the grid; each is the step in swatches.\nconst STEPS: Record<string, number> = {\n ArrowRight: 1,\n ArrowLeft: -1,\n ArrowDown: COLUMNS,\n ArrowUp: -COLUMNS,\n}\n\n// Inside a modal the popover renders in the modal's container. A popover on\n// the page body can't take focus, since the modal pulls it back, and the\n// modal's outer box ignores the pointer, so only its container takes clicks.\nconst modalOrPage = (trigger: HTMLElement) =>\n trigger.closest<HTMLElement>('.ant-modal-container') ?? document.body\n\n// The first three channels of a painted color, 0 to 255. Browsers report\n// `rgb(…)` for most colors and `color(srgb …)`, 0 to 1, for color-mix results.\nconst channels = (painted: string) => {\n const numbers = painted.match(/[\\d.]+/g)?.map(Number)\n if (!numbers || numbers.length < 3) return null\n const scale = painted.startsWith('color(') ? 255 : 1\n return numbers.slice(0, 3).map((n) => n * scale)\n}\n\n// Whether a swatch needs a light check mark: Rec. 601 luma, which is enough\n// for a light-or-dark decision.\nconst isDark = (painted: string) => {\n const rgb = channels(painted)\n return rgb ? (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000 < 140 : true\n}\n\n// Colors may be CSS variables, so the check reads its swatch as painted.\nconst Check = () => {\n const ref = useRef<HTMLSpanElement>(null)\n const [dark, setDark] = useState(true)\n useLayoutEffect(() => {\n const swatch = ref.current?.parentElement\n if (swatch) setDark(isDark(getComputedStyle(swatch).backgroundColor))\n }, [])\n return (\n <span ref={ref} className=\"flex\">\n <CheckIcon\n size={12}\n weight=\"bold\"\n color=\"currentColor\"\n className={cx('block', dark ? 'text-white' : 'text-j2-text')}\n />\n </span>\n )\n}\n\n// The grid and its Reset link. Focus rests on one swatch at a time (roving\n// tabindex), so the grid is one Tab stop and the arrow keys move within it;\n// Enter or Space picks. It starts on the current color, or the first when\n// that isn't in the grid.\nconst SwatchGrid = ({\n colors,\n value,\n defaultValue,\n label,\n onPick,\n gridRef,\n size,\n}: {\n colors: SwatchPickerColor[]\n value: string\n defaultValue?: string\n label: string\n onPick: (value: string) => void\n gridRef?: RefObject<HTMLDivElement | null>\n /** Swatch side in px. */\n size: number\n}) => {\n const [focusIndex, setFocusIndex] = useState(() =>\n Math.max(\n 0,\n colors.findIndex((c) => c.value === value)\n )\n )\n const swatches = useRef<(HTMLButtonElement | null)[]>([])\n\n const onKeyDown = (event: KeyboardEvent) => {\n const step = STEPS[event.key]\n if (step === undefined) return\n event.preventDefault()\n const next = focusIndex + step\n if (next < 0 || next >= colors.length) return\n setFocusIndex(next)\n swatches.current[next]?.focus()\n }\n\n return (\n <div className=\"flex flex-col gap-2\">\n <div\n ref={gridRef}\n role=\"radiogroup\"\n aria-label={`Color for ${label}`}\n // Sized to its swatches: in a wider box the columns would stretch,\n // spreading the swatches and each button past its square.\n className=\"grid w-fit grid-cols-8 gap-1\"\n onKeyDown={onKeyDown}\n >\n {colors.map((c, index) => {\n const selected = c.value === value\n return (\n <button\n key={c.value}\n ref={(node) => {\n swatches.current[index] = node\n }}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n aria-label={c.label ?? c.value}\n tabIndex={index === focusIndex ? 0 : -1}\n onClick={() => {\n // Inline the grid stays, so arrow keys go on from the pick.\n setFocusIndex(index)\n onPick(c.value)\n }}\n className=\"flex cursor-pointer rounded-j2-xs border-0 bg-transparent p-0\"\n >\n <Swatch\n color={c.color ?? c.value}\n size={size}\n radius={RADIUS}\n style={CENTERED}\n >\n {selected && <Check />}\n </Swatch>\n </button>\n )\n })}\n </div>\n {/* Only offered when it would change something. */}\n {defaultValue !== undefined && value !== defaultValue && (\n <Button\n type=\"link\"\n size=\"small\"\n className=\"self-start !px-0\"\n onClick={() => onPick(defaultValue)}\n >\n Reset to default\n </Button>\n )}\n </div>\n )\n}\n\n/**\n * Pick one color from a fixed set, in a popover or inline. A grid rather than\n * a free color input, so two items can't end up on colors that are hard to\n * tell apart. The popover opens on the current color and closes on a pick.\n */\nexport const SwatchPicker = ({\n colors,\n value,\n label,\n onChange,\n defaultValue,\n variant = 'popover',\n children,\n placement = 'rightTop',\n getPopupContainer = modalOrPage,\n onOpenChange,\n}: SwatchPickerProps) => {\n const [open, setOpenState] = useState(false)\n const setOpen = (next: boolean) => {\n setOpenState(next)\n onOpenChange?.(next)\n }\n const gridRef = useRef<HTMLDivElement>(null)\n const triggerRef = useRef<HTMLSpanElement>(null)\n\n // Focus moves into the grid on open, onto the swatch that is its Tab stop:\n // a frame later, once the popover is in the page. (afterOpenChange waits\n // for the open animation.)\n useEffect(() => {\n if (!open) return\n const frame = requestAnimationFrame(() =>\n gridRef.current?.querySelector<HTMLElement>('[tabindex=\"0\"]')?.focus()\n )\n return () => cancelAnimationFrame(frame)\n }, [open])\n\n // A pick or Escape closes it and returns focus to the control that opened it.\n const close = () => {\n setOpen(false)\n triggerRef.current\n ?.querySelector<HTMLElement>('button, [tabindex]')\n ?.focus()\n }\n\n if (variant === 'inline') {\n return (\n <SwatchGrid\n colors={colors}\n value={value}\n defaultValue={defaultValue}\n label={label}\n onPick={onChange}\n size={20}\n />\n )\n }\n\n return (\n <Popover\n trigger=\"click\"\n placement={placement}\n open={open}\n onOpenChange={setOpen}\n getPopupContainer={getPopupContainer}\n destroyOnHidden\n paddingSize=\"xs\"\n content={\n <div\n onKeyDown={(event) => {\n // Only the picker closes, not a modal around it.\n if (event.key !== 'Escape') return\n event.stopPropagation()\n close()\n }}\n >\n <SwatchGrid\n colors={colors}\n value={value}\n defaultValue={defaultValue}\n label={label}\n gridRef={gridRef}\n size={24}\n onPick={(next) => {\n onChange(next)\n close()\n }}\n />\n </div>\n }\n >\n <span ref={triggerRef} className=\"inline-flex\">\n {children}\n </span>\n </Popover>\n )\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@j2-health/design-system",
3
- "version": "0.6.10",
3
+ "version": "0.6.11",
4
4
  "type": "module",
5
5
  "description": "J2 Health shared React component library",
6
6
  "repository": {