@rebasepro/ui 0.25.0-canary.ge06257e → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -241,9 +241,9 @@ var Select = forwardRef(({ inputRef, open, name, fullWidth = false, id, onOpenCh
241
241
  asChild: true,
242
242
  children: /* @__PURE__ */ jsx(ChevronDownIcon, {
243
243
  size: size === "large" ? iconSize.medium : iconSize.small,
244
- className: cls("transition", open ? "rotate-180" : "", {
245
- "px-2": size === "large",
246
- "px-1": size === "medium" || size === "small"
244
+ className: cls("shrink-0 transition", open ? "rotate-180" : "", {
245
+ "ml-2": size === "large",
246
+ "ml-1": size === "medium" || size === "small"
247
247
  })
248
248
  })
249
249
  })]
@@ -1 +1 @@
1
- {"version":3,"file":"Select.es.js","names":[],"sources":["../../src/components/Select.tsx"],"sourcesContent":["\n\"use client\";\nimport { CheckIcon, ChevronDownIcon } from \"lucide-react\";\nimport { iconSize } from \"../icons/Icon\";\nimport React, { ChangeEvent, Children, forwardRef, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport {\n defaultBorderMixin,\n fieldBackgroundDisabledMixin,\n fieldBackgroundHoverMixin,\n fieldBackgroundInvisibleMixin,\n fieldBackgroundMixin,\n focusedDisabled\n} from \"../styles\";\n\nimport { cls } from \"../util\";\nimport { SelectInputLabel } from \"./common/SelectInputLabel\";\nimport { usePortalContainer } from \"../hooks/PortalContainerContext\";\n\nexport type SelectValue = string | number | boolean;\n\n/**\n * Above this many options a closed select renders only the one it shows.\n *\n * Radix renders a closed select's items anyway — into a detached fragment, so\n * that a letter typed on the focused trigger can pick one, and inside a form\n * into the hidden native `<select>` as well. An enum field paid for its whole\n * list on every render of the form around it: 500 options were 100 ms to mount\n * and 17 ms per re-render in jsdom, linear in the count. Small lists keep\n * Radix's own behaviour untouched; long ones answer typed letters here\n * ({@link findNextOption}) and render their items when opened.\n */\nconst LAZY_OPTIONS_THRESHOLD = 50;\n\n/** How long typed letters accumulate into one search — Radix's own window. */\nconst TYPEAHEAD_RESET_MS = 1000;\n\ntype SelectOption = {\n element: React.ReactElement<SelectItemProps<SelectValue>>,\n value: string,\n disabled: boolean\n};\n\n/**\n * The `SelectItem`s among `children`, in document order: through fragments,\n * `SelectGroup`s and any other element wrapping them. An item rendered by a\n * component of the caller's own is not seen, so such a list never counts as\n * long and keeps the default behaviour.\n */\nfunction collectOptions(children: React.ReactNode, into: SelectOption[] = []): SelectOption[] {\n Children.forEach(children, (child) => {\n if (!React.isValidElement(child)) return;\n if (child.type === SelectItem) {\n const props = child.props as SelectItemProps<SelectValue>;\n into.push({\n element: child as React.ReactElement<SelectItemProps<SelectValue>>,\n value: String(props.value),\n disabled: Boolean(props.disabled)\n });\n return;\n }\n const nested = (child.props as { children?: React.ReactNode }).children;\n if (nested !== undefined) collectOptions(nested, into);\n });\n return into;\n}\n\n/** The text a node would render, as far as its elements' own children say. */\nfunction textOf(node: React.ReactNode): string {\n if (node === null || node === undefined || typeof node === \"boolean\") return \"\";\n if (typeof node === \"string\" || typeof node === \"number\") return String(node);\n if (Array.isArray(node)) return node.map(textOf).join(\"\");\n if (React.isValidElement(node)) return textOf((node.props as { children?: React.ReactNode }).children);\n return \"\";\n}\n\n/**\n * What a typed letter matches an option against: its `textValue`, else the\n * text of its children, else its value. Radix reads the rendered text, which\n * an unrendered item does not have — an item whose label is drawn by a\n * component (a chip) passes `textValue`.\n */\nfunction optionLabel(option: SelectOption): string {\n const { textValue, children } = option.element.props;\n if (textValue !== undefined) return textValue;\n return textOf(children).trim() || option.value;\n}\n\n/**\n * Radix's `findNextItem`, over options that are not rendered: the next option\n * after the current one whose label starts with the search; the same letter\n * typed again cycles through the options it starts.\n */\nfunction findNextOption(options: SelectOption[], search: string, current: SelectOption | undefined): SelectOption | undefined {\n if (options.length === 0) return undefined;\n const isRepeated = search.length > 1 && Array.from(search).every((char) => char === search[0]);\n const normalizedSearch = (isRepeated ? search[0] : search).toLowerCase();\n const start = Math.max(current ? options.indexOf(current) : -1, 0);\n let wrapped = options.map((_, index) => options[(start + index) % options.length]);\n if (normalizedSearch.length === 1) wrapped = wrapped.filter((option) => option !== current);\n const next = wrapped.find((option) => optionLabel(option).toLowerCase().startsWith(normalizedSearch));\n return next !== current ? next : undefined;\n}\n\nexport type SelectProps<T extends SelectValue = string> = {\n open?: boolean,\n name?: string,\n fullWidth?: boolean,\n id?: string,\n onOpenChange?: (open: boolean) => void,\n value?: T,\n className?: string,\n inputClassName?: string,\n viewportClassName?: string,\n onChange?: React.EventHandler<ChangeEvent<HTMLSelectElement>>,\n onValueChange?: (updatedValue: T) => void,\n placeholder?: React.ReactNode,\n renderValue?: (value: T) => React.ReactNode,\n size?: \"smallest\" | \"small\" | \"medium\" | \"large\",\n label?: React.ReactNode | string,\n disabled?: boolean,\n error?: boolean,\n position?: \"item-aligned\" | \"popper\",\n endAdornment?: React.ReactNode,\n inputRef?: React.Ref<HTMLButtonElement>,\n padding?: boolean,\n invisible?: boolean,\n /**\n * Whether the trigger draws its own chevron. A table cell turns it off:\n * the cell draws one opener for every kind of editor, in the same place.\n */\n chevron?: boolean,\n /**\n * Replaces everything the trigger draws — the value, the box, its padding,\n * its height and the chevron — with this node. For a caller that frames\n * the select itself: a table cell passes the preview it shows at rest, so\n * selecting the cell moves nothing.\n */\n triggerContent?: React.ReactNode,\n children?: React.ReactNode;\n dataType?: \"string\" | \"number\" | \"boolean\";\n portalContainer?: HTMLElement | null; // Explicitly added to props type if missing\n /**\n * Accessible name for the trigger, for a select whose `label` is a node\n * rather than a string — or which shows no label at all.\n *\n * Without it such a select announces the literal fallback \"Select an\n * option\", which is indistinguishable from every other unlabelled select\n * on the screen.\n */\n \"aria-label\"?: string;\n};\n\nexport const Select = forwardRef<HTMLDivElement, SelectProps>(({\n inputRef,\n open,\n name,\n fullWidth = false,\n id,\n onOpenChange,\n value,\n onChange,\n onValueChange,\n className,\n inputClassName,\n viewportClassName,\n placeholder,\n renderValue,\n label,\n size = \"large\",\n error,\n disabled,\n padding = true,\n position = \"item-aligned\",\n endAdornment,\n invisible,\n chevron = true,\n triggerContent,\n children,\n dataType = \"string\",\n portalContainer: manualContainer, // Rename to avoid confusion\n \"aria-label\": ariaLabel,\n ...props\n}, ref) => {\n\n const [openInternal, setOpenInternal] = useState(open ?? false);\n\n useEffect(() => {\n setOpenInternal(open ?? false);\n }, [open]);\n\n // Get the portal container from context\n const contextContainer = usePortalContainer();\n\n // Resolve final container (Manual Prop > Context Container > Undefined/Body)\n const finalContainer = (manualContainer ?? contextContainer ?? undefined) as HTMLElement | undefined;\n\n const onValueChangeInternal = useCallback((newValue: string) => {\n let typedValue: SelectValue = newValue;\n if (dataType === \"boolean\") {\n if (newValue === \"true\") typedValue = true;\n else if (newValue === \"false\") typedValue = false;\n } else if (dataType === \"number\") {\n if (!isNaN(Number(newValue)) && newValue.trim() !== \"\") typedValue = Number(newValue);\n }\n\n onValueChange?.(typedValue as SelectValue & typeof value);\n if (onChange) {\n const event = {\n target: {\n name,\n value: typedValue\n }\n } as ChangeEvent<HTMLSelectElement>;\n onChange(event);\n }\n }, [onChange, onValueChange, name, dataType]);\n\n const hasValue = Array.isArray(value) ? value.length > 0 : (value != null && value !== \"\" && value !== undefined);\n const stringValue = value !== undefined ? String(value) : undefined;\n\n // A long list renders its items only while open; closed, it renders the\n // one it shows, which keeps that option in the native `<select>` a form\n // submits. See LAZY_OPTIONS_THRESHOLD.\n const options = useMemo(() => collectOptions(children), [children]);\n const lazyOptions = options.length > LAZY_OPTIONS_THRESHOLD;\n const renderedOptions = !lazyOptions || openInternal\n ? children\n : (options.find((option) => option.value === stringValue)?.element ?? null);\n\n // Type-ahead on the closed trigger of a long list, where Radix has no\n // rendered items to search. Radix's own handler still runs after this one\n // and finds nothing to change; it keeps its own buffer in step, so a space\n // typed mid-search does not open the list, as before.\n const typeahead = useRef<{ search: string, timer?: ReturnType<typeof setTimeout> }>({ search: \"\" });\n useEffect(() => () => clearTimeout(typeahead.current.timer), []);\n const resetTypeahead = useCallback(() => {\n clearTimeout(typeahead.current.timer);\n typeahead.current.search = \"\";\n }, []);\n const onTriggerKeyDown = useCallback((event: React.KeyboardEvent<HTMLButtonElement>) => {\n if (!lazyOptions || disabled) return;\n if (event.ctrlKey || event.altKey || event.metaKey || event.key.length !== 1) return;\n const state = typeahead.current;\n // A space starts nothing: it opens the list.\n if (event.key === \" \" && state.search === \"\") return;\n const search = state.search + event.key;\n state.search = search;\n clearTimeout(state.timer);\n state.timer = setTimeout(() => {\n state.search = \"\";\n }, TYPEAHEAD_RESET_MS);\n const enabled = options.filter((option) => !option.disabled);\n const current = enabled.find((option) => option.value === stringValue);\n const next = findNextOption(enabled, search, current);\n if (next) onValueChangeInternal(next.value);\n }, [lazyOptions, disabled, options, stringValue, onValueChangeInternal]);\n\n // Inside a form Radix mirrors the value into a hidden native `<select>` and\n // reports whatever that select then holds. A closed long list has only its\n // shown option in there, so when the value moves to another option — typed,\n // or set by the form — the select has no match for one commit and reports\n // \"\", a change nobody made, which would clear the field. A real \"\" can only\n // come from an option whose value is \"\".\n const hasEmptyOption = useMemo(() => options.some((option) => option.value === \"\"), [options]);\n const onRootValueChange = useCallback((newValue: string) => {\n if (lazyOptions && newValue === \"\" && !hasEmptyOption) return;\n onValueChangeInternal(newValue);\n }, [lazyOptions, hasEmptyOption, onValueChangeInternal]);\n\n const displayChildren = useMemo(() => {\n if (!hasValue || renderValue) return null;\n\n // Find the child that matches the current value to display its content\n let found: React.ReactNode = null;\n Children.forEach(children, (child) => {\n if (React.isValidElement<SelectItemProps>(child) && String(child.props.value) === String(value)) {\n found = child.props.children;\n }\n });\n return found;\n }, [children, hasValue, renderValue, value]);\n\n // Radix reads a value of \"\" as nothing selected and shows the placeholder\n // slot, whatever the Value's children say. So an item whose value really is\n // \"\" (an \"inherit the default\" choice) is shown through that slot, or the\n // trigger stays blank while it is the selected one.\n const emptyItemDisplay = useMemo(() => {\n if (stringValue !== \"\") return null;\n const emptyItem = Children.toArray(children).find((child): child is React.ReactElement<SelectItemProps> =>\n React.isValidElement<SelectItemProps>(child) && String(child.props.value) === \"\");\n if (!emptyItem) return null;\n return value !== undefined && renderValue ? renderValue(value) : emptyItem.props.children;\n }, [children, renderValue, stringValue, value]);\n\n const bare = triggerContent !== undefined;\n // Radix's value slot is an inline `span` that drops the class it is\n // given, so a chip inside it sits on a text baseline and the slot stands\n // a pixel taller than the chip. A block of its own takes it off the line.\n const bareContent = bare ? <div className={\"flex w-full\"}>{triggerContent}</div> : undefined;\n\n const controlHeight = {\n \"min-h-[28px]\": size === \"smallest\",\n \"min-h-[32px]\": size === \"small\",\n \"min-h-[40px]\": size === \"medium\",\n \"min-h-[48px]\": size === \"large\"\n };\n\n return (\n <SelectPrimitive.Root\n name={name}\n value={stringValue}\n open={openInternal}\n disabled={disabled}\n onValueChange={onRootValueChange}\n onOpenChange={(open) => {\n if (open) resetTypeahead();\n onOpenChange?.(open);\n setOpenInternal(open);\n }}\n {...props}>\n {typeof label === \"string\" ? <SelectInputLabel error={error}>{label}</SelectInputLabel> : label}\n <div className={cls(\n \"select-none\",\n !bare && \"rounded-lg text-sm\",\n !bare && (invisible ? fieldBackgroundInvisibleMixin : fieldBackgroundMixin),\n // An invisible field has no hover fill of its own: whatever frames it (a table cell) carries the hover.\n !bare && (disabled ? fieldBackgroundDisabledMixin : (invisible ? \"\" : fieldBackgroundHoverMixin)),\n \"relative flex items-center\",\n className,\n !bare && controlHeight,\n {\n \"w-fit\": !fullWidth,\n \"w-full\": fullWidth\n }\n )}>\n <SelectPrimitive.Trigger\n ref={inputRef}\n id={id}\n asChild={false}\n type=\"button\"\n onKeyDown={onTriggerKeyDown}\n // A `label` that is an element — which every entity form\n // passes, because the label carries the property's type\n // icon — fell through to a hard-coded English literal, so\n // every enum field in every form announced itself as\n // \"Select an option\". Callers pass `aria-label` now; the\n // literal stays as the last resort, and is unreachable for\n // anything that names itself.\n aria-label={ariaLabel ?? (typeof label === \"string\" ? label : (typeof renderValue === \"string\" ? renderValue : \"Select an option\"))}\n aria-invalid={error || undefined}\n className={cls(\n \"h-full\",\n padding && !bare ? {\n \"px-4\": size === \"large\",\n \"px-3\": size === \"medium\",\n \"px-2\": size === \"small\" || size === \"smallest\"\n } : \"\",\n \"outline-hidden focus:outline-hidden\",\n \"outline-none focus:outline-none\",\n \"select-none\",\n !bare && [\n \"rounded-lg text-sm\",\n error ? \"text-red-500 dark:text-red-600\" : \"focus:text-text-primary dark:focus:text-text-primary-dark\",\n error ? \"border border-red-500 dark:border-red-600\" : \"\",\n disabled ? \"text-surface-accent-600 dark:text-surface-accent-400\" : \"text-surface-accent-800 dark:text-white\",\n controlHeight\n ],\n \"relative flex flex-row items-center\",\n {\n \"w-full\": fullWidth,\n \"w-fit\": !fullWidth\n },\n inputClassName\n )}\n >\n <div\n ref={ref}\n className={cls(\n \"flex-grow max-w-full flex flex-row gap-2 items-center\",\n \"overflow-visible\",\n !bare && controlHeight\n )}>\n <SelectPrimitive.Value\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n }}\n placeholder={bareContent ?? emptyItemDisplay ?? placeholder}\n className={\"w-full\"}>\n\n {bareContent ?? (hasValue && value !== undefined && renderValue\n ? renderValue(value)\n : (displayChildren || placeholder)\n )}\n\n </SelectPrimitive.Value>\n </div>\n\n {chevron && !bare && <SelectPrimitive.Icon asChild>\n <ChevronDownIcon size={size === \"large\" ? iconSize.medium : iconSize.small}\n className={cls(\"transition\", open ? \"rotate-180\" : \"\", {\n \"px-2\": size === \"large\",\n \"px-1\": size === \"medium\" || size === \"small\"\n })}/>\n </SelectPrimitive.Icon>}\n </SelectPrimitive.Trigger>\n\n {endAdornment && (\n <div\n className={cls(\"h-full flex items-center absolute right-0 pr-12\")}\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n }}>\n {endAdornment}\n </div>\n )}\n </div>\n\n {/* Pass the calculated finalContainer */}\n <SelectPrimitive.Portal container={finalContainer}>\n <SelectPrimitive.Content position={position}\n className={cls(focusedDisabled, \"z-50 relative overflow-hidden border bg-surface-card p-2 rounded-lg shadow-lg\", defaultBorderMixin)}>\n <SelectPrimitive.Viewport className={cls(\"p-1\", viewportClassName)}\n style={{ maxHeight: \"var(--radix-select-content-available-height)\" }}>\n {renderedOptions}\n </SelectPrimitive.Viewport>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n </SelectPrimitive.Root>\n );\n});\n\nSelect.displayName = \"Select\";\n\nexport type SelectItemProps<T extends SelectValue = string> = {\n value: T,\n children?: React.ReactNode,\n disabled?: boolean,\n className?: string,\n /**\n * The text a letter typed on the focused select matches this option by.\n * Defaults to the option's text. Set it when the option is drawn by a\n * component (a chip, an icon with a name inside it): a closed select with\n * more than 50 options does not render them, so their text cannot be read.\n */\n textValue?: string,\n};\n\nexport const SelectItem = React.memo(function SelectItem<T extends SelectValue = string>({\n value,\n children,\n disabled,\n className,\n textValue\n}: SelectItemProps<T>) {\n // Convert value to string for Radix UI\n const stringValue = String(value);\n\n return <SelectPrimitive.Item\n key={stringValue}\n value={stringValue}\n disabled={disabled}\n textValue={textValue}\n className={cls(\n \"w-full\",\n \"relative flex items-center p-2 rounded-md text-sm text-surface-accent-700 dark:text-surface-accent-300\",\n \"focus:z-10\",\n \"data-[state=checked]:bg-surface-active focus:bg-surface-hover\",\n \"data-[state=checked]:focus:bg-surface-active\",\n disabled ? \"opacity-50 cursor-not-allowed\" : \"cursor-pointer\",\n \"[&>*]:w-full\",\n \"overflow-visible\",\n className\n )}>\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n <div\n className=\"absolute left-1 data-[state=checked]:block hidden\">\n <CheckIcon size={iconSize.smallest}/>\n </div>\n </SelectPrimitive.Item>;\n});\n\nexport type SelectGroupProps = {\n label: React.ReactNode,\n children: React.ReactNode,\n className?: string\n};\n\nexport const SelectGroup = React.memo(function SelectGroup({\n label,\n children,\n className\n}: SelectGroupProps) {\n // The items go inside the group and the label is Radix's own, so the\n // `role=\"group\"` holds its options and is named by the label.\n return <SelectPrimitive.Group className=\"mt-6 first:mt-2\">\n <SelectPrimitive.Label\n className={cls(\n \"text-xs text-surface-accent-900 dark:text-white uppercase tracking-wider font-semibold\",\n \"px-2 py-2\",\n className\n )}>\n {label}\n </SelectPrimitive.Label>\n {children}\n </SelectPrimitive.Group>;\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"Select.es.js","names":[],"sources":["../../src/components/Select.tsx"],"sourcesContent":["\n\"use client\";\nimport { CheckIcon, ChevronDownIcon } from \"lucide-react\";\nimport { iconSize } from \"../icons/Icon\";\nimport React, { ChangeEvent, Children, forwardRef, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport {\n defaultBorderMixin,\n fieldBackgroundDisabledMixin,\n fieldBackgroundHoverMixin,\n fieldBackgroundInvisibleMixin,\n fieldBackgroundMixin,\n focusedDisabled\n} from \"../styles\";\n\nimport { cls } from \"../util\";\nimport { SelectInputLabel } from \"./common/SelectInputLabel\";\nimport { usePortalContainer } from \"../hooks/PortalContainerContext\";\n\nexport type SelectValue = string | number | boolean;\n\n/**\n * Above this many options a closed select renders only the one it shows.\n *\n * Radix renders a closed select's items anyway — into a detached fragment, so\n * that a letter typed on the focused trigger can pick one, and inside a form\n * into the hidden native `<select>` as well. An enum field paid for its whole\n * list on every render of the form around it: 500 options were 100 ms to mount\n * and 17 ms per re-render in jsdom, linear in the count. Small lists keep\n * Radix's own behaviour untouched; long ones answer typed letters here\n * ({@link findNextOption}) and render their items when opened.\n */\nconst LAZY_OPTIONS_THRESHOLD = 50;\n\n/** How long typed letters accumulate into one search — Radix's own window. */\nconst TYPEAHEAD_RESET_MS = 1000;\n\ntype SelectOption = {\n element: React.ReactElement<SelectItemProps<SelectValue>>,\n value: string,\n disabled: boolean\n};\n\n/**\n * The `SelectItem`s among `children`, in document order: through fragments,\n * `SelectGroup`s and any other element wrapping them. An item rendered by a\n * component of the caller's own is not seen, so such a list never counts as\n * long and keeps the default behaviour.\n */\nfunction collectOptions(children: React.ReactNode, into: SelectOption[] = []): SelectOption[] {\n Children.forEach(children, (child) => {\n if (!React.isValidElement(child)) return;\n if (child.type === SelectItem) {\n const props = child.props as SelectItemProps<SelectValue>;\n into.push({\n element: child as React.ReactElement<SelectItemProps<SelectValue>>,\n value: String(props.value),\n disabled: Boolean(props.disabled)\n });\n return;\n }\n const nested = (child.props as { children?: React.ReactNode }).children;\n if (nested !== undefined) collectOptions(nested, into);\n });\n return into;\n}\n\n/** The text a node would render, as far as its elements' own children say. */\nfunction textOf(node: React.ReactNode): string {\n if (node === null || node === undefined || typeof node === \"boolean\") return \"\";\n if (typeof node === \"string\" || typeof node === \"number\") return String(node);\n if (Array.isArray(node)) return node.map(textOf).join(\"\");\n if (React.isValidElement(node)) return textOf((node.props as { children?: React.ReactNode }).children);\n return \"\";\n}\n\n/**\n * What a typed letter matches an option against: its `textValue`, else the\n * text of its children, else its value. Radix reads the rendered text, which\n * an unrendered item does not have — an item whose label is drawn by a\n * component (a chip) passes `textValue`.\n */\nfunction optionLabel(option: SelectOption): string {\n const { textValue, children } = option.element.props;\n if (textValue !== undefined) return textValue;\n return textOf(children).trim() || option.value;\n}\n\n/**\n * Radix's `findNextItem`, over options that are not rendered: the next option\n * after the current one whose label starts with the search; the same letter\n * typed again cycles through the options it starts.\n */\nfunction findNextOption(options: SelectOption[], search: string, current: SelectOption | undefined): SelectOption | undefined {\n if (options.length === 0) return undefined;\n const isRepeated = search.length > 1 && Array.from(search).every((char) => char === search[0]);\n const normalizedSearch = (isRepeated ? search[0] : search).toLowerCase();\n const start = Math.max(current ? options.indexOf(current) : -1, 0);\n let wrapped = options.map((_, index) => options[(start + index) % options.length]);\n if (normalizedSearch.length === 1) wrapped = wrapped.filter((option) => option !== current);\n const next = wrapped.find((option) => optionLabel(option).toLowerCase().startsWith(normalizedSearch));\n return next !== current ? next : undefined;\n}\n\nexport type SelectProps<T extends SelectValue = string> = {\n open?: boolean,\n name?: string,\n fullWidth?: boolean,\n id?: string,\n onOpenChange?: (open: boolean) => void,\n value?: T,\n className?: string,\n inputClassName?: string,\n viewportClassName?: string,\n onChange?: React.EventHandler<ChangeEvent<HTMLSelectElement>>,\n onValueChange?: (updatedValue: T) => void,\n placeholder?: React.ReactNode,\n renderValue?: (value: T) => React.ReactNode,\n size?: \"smallest\" | \"small\" | \"medium\" | \"large\",\n label?: React.ReactNode | string,\n disabled?: boolean,\n error?: boolean,\n position?: \"item-aligned\" | \"popper\",\n endAdornment?: React.ReactNode,\n inputRef?: React.Ref<HTMLButtonElement>,\n padding?: boolean,\n invisible?: boolean,\n /**\n * Whether the trigger draws its own chevron. A table cell turns it off:\n * the cell draws one opener for every kind of editor, in the same place.\n */\n chevron?: boolean,\n /**\n * Replaces everything the trigger draws — the value, the box, its padding,\n * its height and the chevron — with this node. For a caller that frames\n * the select itself: a table cell passes the preview it shows at rest, so\n * selecting the cell moves nothing.\n */\n triggerContent?: React.ReactNode,\n children?: React.ReactNode;\n dataType?: \"string\" | \"number\" | \"boolean\";\n portalContainer?: HTMLElement | null; // Explicitly added to props type if missing\n /**\n * Accessible name for the trigger, for a select whose `label` is a node\n * rather than a string — or which shows no label at all.\n *\n * Without it such a select announces the literal fallback \"Select an\n * option\", which is indistinguishable from every other unlabelled select\n * on the screen.\n */\n \"aria-label\"?: string;\n};\n\nexport const Select = forwardRef<HTMLDivElement, SelectProps>(({\n inputRef,\n open,\n name,\n fullWidth = false,\n id,\n onOpenChange,\n value,\n onChange,\n onValueChange,\n className,\n inputClassName,\n viewportClassName,\n placeholder,\n renderValue,\n label,\n size = \"large\",\n error,\n disabled,\n padding = true,\n position = \"item-aligned\",\n endAdornment,\n invisible,\n chevron = true,\n triggerContent,\n children,\n dataType = \"string\",\n portalContainer: manualContainer, // Rename to avoid confusion\n \"aria-label\": ariaLabel,\n ...props\n}, ref) => {\n\n const [openInternal, setOpenInternal] = useState(open ?? false);\n\n useEffect(() => {\n setOpenInternal(open ?? false);\n }, [open]);\n\n // Get the portal container from context\n const contextContainer = usePortalContainer();\n\n // Resolve final container (Manual Prop > Context Container > Undefined/Body)\n const finalContainer = (manualContainer ?? contextContainer ?? undefined) as HTMLElement | undefined;\n\n const onValueChangeInternal = useCallback((newValue: string) => {\n let typedValue: SelectValue = newValue;\n if (dataType === \"boolean\") {\n if (newValue === \"true\") typedValue = true;\n else if (newValue === \"false\") typedValue = false;\n } else if (dataType === \"number\") {\n if (!isNaN(Number(newValue)) && newValue.trim() !== \"\") typedValue = Number(newValue);\n }\n\n onValueChange?.(typedValue as SelectValue & typeof value);\n if (onChange) {\n const event = {\n target: {\n name,\n value: typedValue\n }\n } as ChangeEvent<HTMLSelectElement>;\n onChange(event);\n }\n }, [onChange, onValueChange, name, dataType]);\n\n const hasValue = Array.isArray(value) ? value.length > 0 : (value != null && value !== \"\" && value !== undefined);\n const stringValue = value !== undefined ? String(value) : undefined;\n\n // A long list renders its items only while open; closed, it renders the\n // one it shows, which keeps that option in the native `<select>` a form\n // submits. See LAZY_OPTIONS_THRESHOLD.\n const options = useMemo(() => collectOptions(children), [children]);\n const lazyOptions = options.length > LAZY_OPTIONS_THRESHOLD;\n const renderedOptions = !lazyOptions || openInternal\n ? children\n : (options.find((option) => option.value === stringValue)?.element ?? null);\n\n // Type-ahead on the closed trigger of a long list, where Radix has no\n // rendered items to search. Radix's own handler still runs after this one\n // and finds nothing to change; it keeps its own buffer in step, so a space\n // typed mid-search does not open the list, as before.\n const typeahead = useRef<{ search: string, timer?: ReturnType<typeof setTimeout> }>({ search: \"\" });\n useEffect(() => () => clearTimeout(typeahead.current.timer), []);\n const resetTypeahead = useCallback(() => {\n clearTimeout(typeahead.current.timer);\n typeahead.current.search = \"\";\n }, []);\n const onTriggerKeyDown = useCallback((event: React.KeyboardEvent<HTMLButtonElement>) => {\n if (!lazyOptions || disabled) return;\n if (event.ctrlKey || event.altKey || event.metaKey || event.key.length !== 1) return;\n const state = typeahead.current;\n // A space starts nothing: it opens the list.\n if (event.key === \" \" && state.search === \"\") return;\n const search = state.search + event.key;\n state.search = search;\n clearTimeout(state.timer);\n state.timer = setTimeout(() => {\n state.search = \"\";\n }, TYPEAHEAD_RESET_MS);\n const enabled = options.filter((option) => !option.disabled);\n const current = enabled.find((option) => option.value === stringValue);\n const next = findNextOption(enabled, search, current);\n if (next) onValueChangeInternal(next.value);\n }, [lazyOptions, disabled, options, stringValue, onValueChangeInternal]);\n\n // Inside a form Radix mirrors the value into a hidden native `<select>` and\n // reports whatever that select then holds. A closed long list has only its\n // shown option in there, so when the value moves to another option — typed,\n // or set by the form — the select has no match for one commit and reports\n // \"\", a change nobody made, which would clear the field. A real \"\" can only\n // come from an option whose value is \"\".\n const hasEmptyOption = useMemo(() => options.some((option) => option.value === \"\"), [options]);\n const onRootValueChange = useCallback((newValue: string) => {\n if (lazyOptions && newValue === \"\" && !hasEmptyOption) return;\n onValueChangeInternal(newValue);\n }, [lazyOptions, hasEmptyOption, onValueChangeInternal]);\n\n const displayChildren = useMemo(() => {\n if (!hasValue || renderValue) return null;\n\n // Find the child that matches the current value to display its content\n let found: React.ReactNode = null;\n Children.forEach(children, (child) => {\n if (React.isValidElement<SelectItemProps>(child) && String(child.props.value) === String(value)) {\n found = child.props.children;\n }\n });\n return found;\n }, [children, hasValue, renderValue, value]);\n\n // Radix reads a value of \"\" as nothing selected and shows the placeholder\n // slot, whatever the Value's children say. So an item whose value really is\n // \"\" (an \"inherit the default\" choice) is shown through that slot, or the\n // trigger stays blank while it is the selected one.\n const emptyItemDisplay = useMemo(() => {\n if (stringValue !== \"\") return null;\n const emptyItem = Children.toArray(children).find((child): child is React.ReactElement<SelectItemProps> =>\n React.isValidElement<SelectItemProps>(child) && String(child.props.value) === \"\");\n if (!emptyItem) return null;\n return value !== undefined && renderValue ? renderValue(value) : emptyItem.props.children;\n }, [children, renderValue, stringValue, value]);\n\n const bare = triggerContent !== undefined;\n // Radix's value slot is an inline `span` that drops the class it is\n // given, so a chip inside it sits on a text baseline and the slot stands\n // a pixel taller than the chip. A block of its own takes it off the line.\n const bareContent = bare ? <div className={\"flex w-full\"}>{triggerContent}</div> : undefined;\n\n const controlHeight = {\n \"min-h-[28px]\": size === \"smallest\",\n \"min-h-[32px]\": size === \"small\",\n \"min-h-[40px]\": size === \"medium\",\n \"min-h-[48px]\": size === \"large\"\n };\n\n return (\n <SelectPrimitive.Root\n name={name}\n value={stringValue}\n open={openInternal}\n disabled={disabled}\n onValueChange={onRootValueChange}\n onOpenChange={(open) => {\n if (open) resetTypeahead();\n onOpenChange?.(open);\n setOpenInternal(open);\n }}\n {...props}>\n {typeof label === \"string\" ? <SelectInputLabel error={error}>{label}</SelectInputLabel> : label}\n <div className={cls(\n \"select-none\",\n !bare && \"rounded-lg text-sm\",\n !bare && (invisible ? fieldBackgroundInvisibleMixin : fieldBackgroundMixin),\n // An invisible field has no hover fill of its own: whatever frames it (a table cell) carries the hover.\n !bare && (disabled ? fieldBackgroundDisabledMixin : (invisible ? \"\" : fieldBackgroundHoverMixin)),\n \"relative flex items-center\",\n className,\n !bare && controlHeight,\n {\n \"w-fit\": !fullWidth,\n \"w-full\": fullWidth\n }\n )}>\n <SelectPrimitive.Trigger\n ref={inputRef}\n id={id}\n asChild={false}\n type=\"button\"\n onKeyDown={onTriggerKeyDown}\n // A `label` that is an element — which every entity form\n // passes, because the label carries the property's type\n // icon — fell through to a hard-coded English literal, so\n // every enum field in every form announced itself as\n // \"Select an option\". Callers pass `aria-label` now; the\n // literal stays as the last resort, and is unreachable for\n // anything that names itself.\n aria-label={ariaLabel ?? (typeof label === \"string\" ? label : (typeof renderValue === \"string\" ? renderValue : \"Select an option\"))}\n aria-invalid={error || undefined}\n className={cls(\n \"h-full\",\n padding && !bare ? {\n \"px-4\": size === \"large\",\n \"px-3\": size === \"medium\",\n \"px-2\": size === \"small\" || size === \"smallest\"\n } : \"\",\n \"outline-hidden focus:outline-hidden\",\n \"outline-none focus:outline-none\",\n \"select-none\",\n !bare && [\n \"rounded-lg text-sm\",\n error ? \"text-red-500 dark:text-red-600\" : \"focus:text-text-primary dark:focus:text-text-primary-dark\",\n error ? \"border border-red-500 dark:border-red-600\" : \"\",\n disabled ? \"text-surface-accent-600 dark:text-surface-accent-400\" : \"text-surface-accent-800 dark:text-white\",\n controlHeight\n ],\n \"relative flex flex-row items-center\",\n {\n \"w-full\": fullWidth,\n \"w-fit\": !fullWidth\n },\n inputClassName\n )}\n >\n <div\n ref={ref}\n className={cls(\n \"flex-grow max-w-full flex flex-row gap-2 items-center\",\n \"overflow-visible\",\n !bare && controlHeight\n )}>\n <SelectPrimitive.Value\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n }}\n placeholder={bareContent ?? emptyItemDisplay ?? placeholder}\n className={\"w-full\"}>\n\n {bareContent ?? (hasValue && value !== undefined && renderValue\n ? renderValue(value)\n : (displayChildren || placeholder)\n )}\n\n </SelectPrimitive.Value>\n </div>\n\n {/* Spaced with a margin, never padding: under border-box,\n padding on the svg comes out of its width and draws the\n chevron smaller and fainter than the relation field's. */}\n {chevron && !bare && <SelectPrimitive.Icon asChild>\n <ChevronDownIcon size={size === \"large\" ? iconSize.medium : iconSize.small}\n className={cls(\"shrink-0 transition\", open ? \"rotate-180\" : \"\", {\n \"ml-2\": size === \"large\",\n \"ml-1\": size === \"medium\" || size === \"small\"\n })}/>\n </SelectPrimitive.Icon>}\n </SelectPrimitive.Trigger>\n\n {endAdornment && (\n <div\n className={cls(\"h-full flex items-center absolute right-0 pr-12\")}\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n }}>\n {endAdornment}\n </div>\n )}\n </div>\n\n {/* Pass the calculated finalContainer */}\n <SelectPrimitive.Portal container={finalContainer}>\n <SelectPrimitive.Content position={position}\n className={cls(focusedDisabled, \"z-50 relative overflow-hidden border bg-surface-card p-2 rounded-lg shadow-lg\", defaultBorderMixin)}>\n <SelectPrimitive.Viewport className={cls(\"p-1\", viewportClassName)}\n style={{ maxHeight: \"var(--radix-select-content-available-height)\" }}>\n {renderedOptions}\n </SelectPrimitive.Viewport>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n </SelectPrimitive.Root>\n );\n});\n\nSelect.displayName = \"Select\";\n\nexport type SelectItemProps<T extends SelectValue = string> = {\n value: T,\n children?: React.ReactNode,\n disabled?: boolean,\n className?: string,\n /**\n * The text a letter typed on the focused select matches this option by.\n * Defaults to the option's text. Set it when the option is drawn by a\n * component (a chip, an icon with a name inside it): a closed select with\n * more than 50 options does not render them, so their text cannot be read.\n */\n textValue?: string,\n};\n\nexport const SelectItem = React.memo(function SelectItem<T extends SelectValue = string>({\n value,\n children,\n disabled,\n className,\n textValue\n}: SelectItemProps<T>) {\n // Convert value to string for Radix UI\n const stringValue = String(value);\n\n return <SelectPrimitive.Item\n key={stringValue}\n value={stringValue}\n disabled={disabled}\n textValue={textValue}\n className={cls(\n \"w-full\",\n \"relative flex items-center p-2 rounded-md text-sm text-surface-accent-700 dark:text-surface-accent-300\",\n \"focus:z-10\",\n \"data-[state=checked]:bg-surface-active focus:bg-surface-hover\",\n \"data-[state=checked]:focus:bg-surface-active\",\n disabled ? \"opacity-50 cursor-not-allowed\" : \"cursor-pointer\",\n \"[&>*]:w-full\",\n \"overflow-visible\",\n className\n )}>\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n <div\n className=\"absolute left-1 data-[state=checked]:block hidden\">\n <CheckIcon size={iconSize.smallest}/>\n </div>\n </SelectPrimitive.Item>;\n});\n\nexport type SelectGroupProps = {\n label: React.ReactNode,\n children: React.ReactNode,\n className?: string\n};\n\nexport const SelectGroup = React.memo(function SelectGroup({\n label,\n children,\n className\n}: SelectGroupProps) {\n // The items go inside the group and the label is Radix's own, so the\n // `role=\"group\"` holds its options and is named by the label.\n return <SelectPrimitive.Group className=\"mt-6 first:mt-2\">\n <SelectPrimitive.Label\n className={cls(\n \"text-xs text-surface-accent-900 dark:text-white uppercase tracking-wider font-semibold\",\n \"px-2 py-2\",\n className\n )}>\n {label}\n </SelectPrimitive.Label>\n {children}\n </SelectPrimitive.Group>;\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAM,yBAAyB;;AAG/B,IAAM,qBAAqB;;;;;;;AAc3B,SAAS,eAAe,UAA2B,OAAuB,CAAC,GAAmB;CAC1F,SAAS,QAAQ,WAAW,UAAU;EAClC,IAAI,CAAC,MAAM,eAAe,KAAK,GAAG;EAClC,IAAI,MAAM,SAAS,YAAY;GAC3B,MAAM,QAAQ,MAAM;GACpB,KAAK,KAAK;IACN,SAAS;IACT,OAAO,OAAO,MAAM,KAAK;IACzB,UAAU,QAAQ,MAAM,QAAQ;GACpC,CAAC;GACD;EACJ;EACA,MAAM,SAAU,MAAM,MAAyC;EAC/D,IAAI,WAAW,KAAA,GAAW,eAAe,QAAQ,IAAI;CACzD,CAAC;CACD,OAAO;AACX;;AAGA,SAAS,OAAO,MAA+B;CAC3C,IAAI,SAAS,QAAQ,SAAS,KAAA,KAAa,OAAO,SAAS,WAAW,OAAO;CAC7E,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU,OAAO,OAAO,IAAI;CAC5E,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO,KAAK,IAAI,MAAM,CAAC,CAAC,KAAK,EAAE;CACxD,IAAI,MAAM,eAAe,IAAI,GAAG,OAAO,OAAQ,KAAK,MAAyC,QAAQ;CACrG,OAAO;AACX;;;;;;;AAQA,SAAS,YAAY,QAA8B;CAC/C,MAAM,EAAE,WAAW,aAAa,OAAO,QAAQ;CAC/C,IAAI,cAAc,KAAA,GAAW,OAAO;CACpC,OAAO,OAAO,QAAQ,CAAC,CAAC,KAAK,KAAK,OAAO;AAC7C;;;;;;AAOA,SAAS,eAAe,SAAyB,QAAgB,SAA6D;CAC1H,IAAI,QAAQ,WAAW,GAAG,OAAO,KAAA;CAEjC,MAAM,oBADa,OAAO,SAAS,KAAK,MAAM,KAAK,MAAM,CAAC,CAAC,OAAO,SAAS,SAAS,OAAO,EAAE,IACtD,OAAO,KAAK,OAAA,CAAQ,YAAY;CACvE,MAAM,QAAQ,KAAK,IAAI,UAAU,QAAQ,QAAQ,OAAO,IAAI,IAAI,CAAC;CACjE,IAAI,UAAU,QAAQ,KAAK,GAAG,UAAU,SAAS,QAAQ,SAAS,QAAQ,OAAO;CACjF,IAAI,iBAAiB,WAAW,GAAG,UAAU,QAAQ,QAAQ,WAAW,WAAW,OAAO;CAC1F,MAAM,OAAO,QAAQ,MAAM,WAAW,YAAY,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,WAAW,gBAAgB,CAAC;CACpG,OAAO,SAAS,UAAU,OAAO,KAAA;AACrC;AAmDA,IAAa,SAAS,YAAyC,EAC3D,UACA,MACA,MACA,YAAY,OACZ,IACA,cACA,OACA,UACA,eACA,WACA,gBACA,mBACA,aACA,aACA,OACA,OAAO,SACP,OACA,UACA,UAAU,MACV,WAAW,gBACX,cACA,WACA,UAAU,MACV,gBACA,UACA,WAAW,UACX,iBAAiB,iBACjB,cAAc,WACd,GAAG,SACJ,QAAQ;CAEP,MAAM,CAAC,cAAc,mBAAmB,SAAS,QAAQ,KAAK;CAE9D,gBAAgB;EACZ,gBAAgB,QAAQ,KAAK;CACjC,GAAG,CAAC,IAAI,CAAC;CAGT,MAAM,mBAAmB,mBAAmB;CAG5C,MAAM,iBAAkB,mBAAmB,oBAAoB,KAAA;CAE/D,MAAM,wBAAwB,aAAa,aAAqB;EAC5D,IAAI,aAA0B;EAC9B,IAAI,aAAa;OACT,aAAa,QAAQ,aAAa;QACjC,IAAI,aAAa,SAAS,aAAa;EAAA,OACzC,IAAI,aAAa;OAChB,CAAC,MAAM,OAAO,QAAQ,CAAC,KAAK,SAAS,KAAK,MAAM,IAAI,aAAa,OAAO,QAAQ;EAAA;EAGxF,gBAAgB,UAAwC;EACxD,IAAI,UAOA,SAAS,EALL,QAAQ;GACJ;GACA,OAAO;EACX,EAEK,CAAK;CAEtB,GAAG;EAAC;EAAU;EAAe;EAAM;CAAQ,CAAC;CAE5C,MAAM,WAAW,MAAM,QAAQ,KAAK,IAAI,MAAM,SAAS,IAAK,SAAS,QAAQ,UAAU,MAAM,UAAU,KAAA;CACvG,MAAM,cAAc,UAAU,KAAA,IAAY,OAAO,KAAK,IAAI,KAAA;CAK1D,MAAM,UAAU,cAAc,eAAe,QAAQ,GAAG,CAAC,QAAQ,CAAC;CAClE,MAAM,cAAc,QAAQ,SAAS;CACrC,MAAM,kBAAkB,CAAC,eAAe,eAClC,WACC,QAAQ,MAAM,WAAW,OAAO,UAAU,WAAW,CAAC,EAAE,WAAW;CAM1E,MAAM,YAAY,OAAkE,EAAE,QAAQ,GAAG,CAAC;CAClG,sBAAsB,aAAa,UAAU,QAAQ,KAAK,GAAG,CAAC,CAAC;CAC/D,MAAM,iBAAiB,kBAAkB;EACrC,aAAa,UAAU,QAAQ,KAAK;EACpC,UAAU,QAAQ,SAAS;CAC/B,GAAG,CAAC,CAAC;CACL,MAAM,mBAAmB,aAAa,UAAkD;EACpF,IAAI,CAAC,eAAe,UAAU;EAC9B,IAAI,MAAM,WAAW,MAAM,UAAU,MAAM,WAAW,MAAM,IAAI,WAAW,GAAG;EAC9E,MAAM,QAAQ,UAAU;EAExB,IAAI,MAAM,QAAQ,OAAO,MAAM,WAAW,IAAI;EAC9C,MAAM,SAAS,MAAM,SAAS,MAAM;EACpC,MAAM,SAAS;EACf,aAAa,MAAM,KAAK;EACxB,MAAM,QAAQ,iBAAiB;GAC3B,MAAM,SAAS;EACnB,GAAG,kBAAkB;EACrB,MAAM,UAAU,QAAQ,QAAQ,WAAW,CAAC,OAAO,QAAQ;EAE3D,MAAM,OAAO,eAAe,SAAS,QADrB,QAAQ,MAAM,WAAW,OAAO,UAAU,WACb,CAAO;EACpD,IAAI,MAAM,sBAAsB,KAAK,KAAK;CAC9C,GAAG;EAAC;EAAa;EAAU;EAAS;EAAa;CAAqB,CAAC;CAQvE,MAAM,iBAAiB,cAAc,QAAQ,MAAM,WAAW,OAAO,UAAU,EAAE,GAAG,CAAC,OAAO,CAAC;CAC7F,MAAM,oBAAoB,aAAa,aAAqB;EACxD,IAAI,eAAe,aAAa,MAAM,CAAC,gBAAgB;EACvD,sBAAsB,QAAQ;CAClC,GAAG;EAAC;EAAa;EAAgB;CAAqB,CAAC;CAEvD,MAAM,kBAAkB,cAAc;EAClC,IAAI,CAAC,YAAY,aAAa,OAAO;EAGrC,IAAI,QAAyB;EAC7B,SAAS,QAAQ,WAAW,UAAU;GAClC,IAAI,MAAM,eAAgC,KAAK,KAAK,OAAO,MAAM,MAAM,KAAK,MAAM,OAAO,KAAK,GAC1F,QAAQ,MAAM,MAAM;EAE5B,CAAC;EACD,OAAO;CACX,GAAG;EAAC;EAAU;EAAU;EAAa;CAAK,CAAC;CAM3C,MAAM,mBAAmB,cAAc;EACnC,IAAI,gBAAgB,IAAI,OAAO;EAC/B,MAAM,YAAY,SAAS,QAAQ,QAAQ,CAAC,CAAC,MAAM,UAC/C,MAAM,eAAgC,KAAK,KAAK,OAAO,MAAM,MAAM,KAAK,MAAM,EAAE;EACpF,IAAI,CAAC,WAAW,OAAO;EACvB,OAAO,UAAU,KAAA,KAAa,cAAc,YAAY,KAAK,IAAI,UAAU,MAAM;CACrF,GAAG;EAAC;EAAU;EAAa;EAAa;CAAK,CAAC;CAE9C,MAAM,OAAO,mBAAmB,KAAA;CAIhC,MAAM,cAAc,OAAO,oBAAC,OAAD;EAAK,WAAW;YAAgB;CAAoB,CAAA,IAAI,KAAA;CAEnF,MAAM,gBAAgB;EAClB,gBAAgB,SAAS;EACzB,gBAAgB,SAAS;EACzB,gBAAgB,SAAS;EACzB,gBAAgB,SAAS;CAC7B;CAEA,OACI,qBAAC,gBAAgB,MAAjB;EACU;EACN,OAAO;EACP,MAAM;EACI;EACV,eAAe;EACf,eAAe,SAAS;GACpB,IAAI,MAAM,eAAe;GACzB,eAAe,IAAI;GACnB,gBAAgB,IAAI;EACxB;EACA,GAAI;YAXR;GAYK,OAAO,UAAU,WAAW,oBAAC,kBAAD;IAAyB;cAAQ;GAAwB,CAAA,IAAI;GAC1F,qBAAC,OAAD;IAAK,WAAW,IACZ,eACA,CAAC,QAAQ,sBACT,CAAC,SAAS,YAAA,mBAAA,4CAEV,CAAC,SAAS,WAAA,wBAA2C,YAAY,KAAA,gEACjE,8BACA,WACA,CAAC,QAAQ,eACT;KACI,SAAS,CAAC;KACV,UAAU;IACd,CACJ;cAbA,CAcI,qBAAC,gBAAgB,SAAjB;KACI,KAAK;KACD;KACJ,SAAS;KACT,MAAK;KACL,WAAW;KAQX,cAAY,cAAc,OAAO,UAAU,WAAW,QAAS,OAAO,gBAAgB,WAAW,cAAc;KAC/G,gBAAc,SAAS,KAAA;KACvB,WAAW,IACP,UACA,WAAW,CAAC,OAAO;MACf,QAAQ,SAAS;MACjB,QAAQ,SAAS;MACjB,QAAQ,SAAS,WAAW,SAAS;KACzC,IAAI,IACJ,uCACA,mCACA,eACA,CAAC,QAAQ;MACL;MACA,QAAQ,mCAAmC;MAC3C,QAAQ,8CAA8C;MACtD,WAAW,yDAAyD;MACpE;KACJ,GACA,uCACA;MACI,UAAU;MACV,SAAS,CAAC;KACd,GACA,cACJ;eAtCJ,CAwCI,oBAAC,OAAD;MACS;MACL,WAAW,IACP,yDACA,oBACA,CAAC,QAAQ,aACb;gBACA,oBAAC,gBAAgB,OAAjB;OACI,UAAU,MAAM;QACZ,EAAE,eAAe;QACjB,EAAE,gBAAgB;OACtB;OACA,aAAa,eAAe,oBAAoB;OAChD,WAAW;iBAEV,gBAAgB,YAAY,UAAU,KAAA,KAAa,cAC9C,YAAY,KAAK,IAChB,mBAAmB;MAGP,CAAA;KACtB,CAAA,GAKJ,WAAW,CAAC,QAAQ,oBAAC,gBAAgB,MAAjB;MAAsB,SAAA;gBACvC,oBAAC,iBAAD;OAAiB,MAAM,SAAS,UAAU,SAAS,SAAS,SAAS;OACjE,WAAW,IAAI,uBAAuB,OAAO,eAAe,IAAI;QAC5D,QAAQ,SAAS;QACjB,QAAQ,SAAS,YAAY,SAAS;OAC1C,CAAC;MAAG,CAAA;KACU,CAAA,CACD;QAExB,gBACG,oBAAC,OAAD;KACI,WAAW,IAAI,iDAAiD;KAChE,UAAU,MAAM;MACZ,EAAE,eAAe;MACjB,EAAE,gBAAgB;KACtB;eACC;IACA,CAAA,CAER;;GAGL,oBAAC,gBAAgB,QAAjB;IAAwB,WAAW;cAC/B,oBAAC,gBAAgB,SAAjB;KAAmC;KAC/B,WAAW,IAAI,iBAAiB,iFAAiF,kBAAkB;eACnI,oBAAC,gBAAgB,UAAjB;MAA0B,WAAW,IAAI,OAAO,iBAAiB;MAC7D,OAAO,EAAE,WAAW,+CAA+C;gBAClE;KACqB,CAAA;IACL,CAAA;GACL,CAAA;EACN;;AAE9B,CAAC;AAED,OAAO,cAAc;AAgBrB,IAAa,aAAa,MAAM,KAAK,SAAS,WAA2C,EACrF,OACA,UACA,UACA,WACA,aACmB;CAEnB,MAAM,cAAc,OAAO,KAAK;CAEhC,OAAO,qBAAC,gBAAgB,MAAjB;EAEH,OAAO;EACG;EACC;EACX,WAAW,IACP,UACA,0GACA,cACA,iEACA,gDACA,WAAW,kCAAkC,kBAC7C,gBACA,oBACA,SACJ;YAfG,CAgBH,oBAAC,gBAAgB,UAAjB,EAA2B,SAAmC,CAAA,GAC9D,oBAAC,OAAD;GACI,WAAU;aACV,oBAAC,WAAD,EAAW,MAAM,SAAS,SAAU,CAAA;EACnC,CAAA,CACa;IApBb,WAoBa;AAC1B,CAAC;AAQD,IAAa,cAAc,MAAM,KAAK,SAAS,YAAY,EACvD,OACA,UACA,aACiB;CAGjB,OAAO,qBAAC,gBAAgB,OAAjB;EAAuB,WAAU;YAAjC,CACH,oBAAC,gBAAgB,OAAjB;GACI,WAAW,IACP,0FACA,aACA,SACJ;aACC;EACkB,CAAA,GACtB,QACkB;;AAC3B,CAAC"}
@@ -9,8 +9,8 @@ import React from "react";
9
9
  * Measured side by side on /debug/ui, which is where it is most obvious, because
10
10
  * the reference puts them in adjacent columns.
11
11
  *
12
- * The text tokens are also the tuned ones: `--color-surface-500` was moved to
13
- * #797979 precisely so muted type clears AA on both grounds. The slate scale
12
+ * The text tokens are also the tuned ones: `--color-surface-500` sits at the
13
+ * lightness where muted type clears AA on both grounds. The accent scale
14
14
  * carries no such guarantee — it is for surfaces.
15
15
  */
16
16
  export declare function SelectInputLabel({ children, error }: {
@@ -12,8 +12,8 @@ import { jsx } from "react/jsx-runtime";
12
12
  * Measured side by side on /debug/ui, which is where it is most obvious, because
13
13
  * the reference puts them in adjacent columns.
14
14
  *
15
- * The text tokens are also the tuned ones: `--color-surface-500` was moved to
16
- * #797979 precisely so muted type clears AA on both grounds. The slate scale
15
+ * The text tokens are also the tuned ones: `--color-surface-500` sits at the
16
+ * lightness where muted type clears AA on both grounds. The accent scale
17
17
  * carries no such guarantee — it is for surfaces.
18
18
  */
19
19
  function SelectInputLabel({ children, error }) {
@@ -1 +1 @@
1
- {"version":3,"file":"SelectInputLabel.es.js","names":[],"sources":["../../../src/components/common/SelectInputLabel.tsx"],"sourcesContent":["import React from \"react\";\nimport { cls } from \"../../util\";\n\n/**\n * The label above a Select or MultiSelect.\n *\n * It used `text-surface-accent-500` — a SURFACE scale, borrowed for type. The\n * label above a TextField has always used `text-text-secondary`, and the two are\n * the same role, so a form mixing the components showed two different label\n * colours: `#64748b` blue-grey over a Select, `#757575` neutral over a TextField.\n * Measured side by side on /debug/ui, which is where it is most obvious, because\n * the reference puts them in adjacent columns.\n *\n * The text tokens are also the tuned ones: `--color-surface-500` was moved to\n * #797979 precisely so muted type clears AA on both grounds. The slate scale\n * carries no such guarantee — it is for surfaces.\n */\nexport function SelectInputLabel({ children, error }: { children: React.ReactNode, error?: boolean }) {\n return <div className={cls(\"text-sm font-medium ml-3.5 mb-1\",\n error ? \"text-red-600 dark:text-red-500\" : \"text-text-secondary dark:text-text-secondary-dark\")}>\n {children}\n </div>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,SAAgB,iBAAiB,EAAE,UAAU,SAAyD;CAClG,OAAO,oBAAC,OAAD;EAAK,WAAW,IAAI,mCACvB,QAAQ,mCAAmC,mDAAmD;EAC7F;CACA,CAAA;AACT"}
1
+ {"version":3,"file":"SelectInputLabel.es.js","names":[],"sources":["../../../src/components/common/SelectInputLabel.tsx"],"sourcesContent":["import React from \"react\";\nimport { cls } from \"../../util\";\n\n/**\n * The label above a Select or MultiSelect.\n *\n * It used `text-surface-accent-500` — a SURFACE scale, borrowed for type. The\n * label above a TextField has always used `text-text-secondary`, and the two are\n * the same role, so a form mixing the components showed two different label\n * colours: `#64748b` blue-grey over a Select, `#757575` neutral over a TextField.\n * Measured side by side on /debug/ui, which is where it is most obvious, because\n * the reference puts them in adjacent columns.\n *\n * The text tokens are also the tuned ones: `--color-surface-500` sits at the\n * lightness where muted type clears AA on both grounds. The accent scale\n * carries no such guarantee — it is for surfaces.\n */\nexport function SelectInputLabel({ children, error }: { children: React.ReactNode, error?: boolean }) {\n return <div className={cls(\"text-sm font-medium ml-3.5 mb-1\",\n error ? \"text-red-600 dark:text-red-500\" : \"text-text-secondary dark:text-text-secondary-dark\")}>\n {children}\n </div>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,SAAgB,iBAAiB,EAAE,UAAU,SAAyD;CAClG,OAAO,oBAAC,OAAD;EAAK,WAAW,IAAI,mCACvB,QAAQ,mCAAmC,mDAAmD;EAC7F;CACA,CAAA;AACT"}
@@ -115,68 +115,94 @@
115
115
  --color-primary-bg: oklch(from var(--color-primary) l c h / 0.1);
116
116
  --color-secondary-bg: oklch(from var(--color-secondary) l c h / 0.1);
117
117
 
118
+ /* GRAPHITE — every neutral in this file, text and surface, sits on ONE hue:
119
+ 258°, the brand blue's own, at a chroma low enough (0.002-0.014) that
120
+ nothing reads as blue. Adopted 2026-10-14.
121
+
122
+ Before, the neutrals sat on five hues: the surface ladder had none (pure
123
+ grey), the text greys were Tailwind's zinc at 286° (violet), the site and
124
+ film grounds 246°, the slate scale 264°. A hue-less grey beside a cool
125
+ black reads warm and muddy, so a card looked like a grey box set on
126
+ black rather than a lifted piece of the same surface.
127
+
128
+ Each value is `oklch(L c 258)` with the lightness it had before, so
129
+ contrast and the ladder's steps did not move; the exceptions are the
130
+ text tiers noted below, moved on purpose. The values are written as hex
131
+ because tokens are read by more than CSS (canvas, email, charts), and a
132
+ guard (test/neutral-hue.test.ts) fails on any neutral off the hue. */
133
+
118
134
  /* Field Colors */
119
- --color-field-disabled: rgb(224 224 226);
120
- --color-field-disabled-dark: rgb(35 35 37);
135
+ --color-field-disabled: #e1e4e7;
136
+ --color-field-disabled-dark: #212429;
121
137
 
122
138
  /* Text Colors — using opaque values so icon strokes render solidly */
123
- --color-text-primary: #212121;
124
- /* #737373, not #757575. Two units, and they are the two that matter.
125
- The app's light page ground is #fafafa, not #ffffff, and #757575 lands at
126
- 4.41:1 there — under AA for the 14px type this token carries. It clears
127
- 4.5 only against pure white, which is not a surface the product actually
128
- uses. Every field label in light mode was failing by a whisker.
129
- #737373 measures 4.54:1 on #fafafa and 4.74:1 on #ffffff, so it passes on
130
- both grounds instead of the theoretical one. Same fix, and the same
131
- reasoning, as the `--color-surface-500` move recorded below. */
132
- --color-text-secondary: #737373;
133
- /* #8b8b8b, not #9e9e9e. Measured against the field fill a disabled label
134
- actually sits on (`surface-accent-200/50` over #fafafa ≈ #eef1f5),
135
- #9e9e9e reached 2.36:1 — present, but not readable.
136
- Full AA is not achievable here and should not be chased: 4.5:1 on that
137
- fill needs #6e6e6e, which is DARKER than the #737373 an ENABLED label
138
- uses, so the disabled state would read as the emphasised one. WCAG 1.4.3
139
- exempts inactive controls for exactly this reason.
140
- #8b8b8b lands at ~3:1 — legible at a glance, still unmistakably muted
141
- against its enabled neighbour. */
142
- --color-text-disabled: #8b8b8b;
143
- --color-text-primary-dark: #ffffff;
144
- --color-text-secondary-dark: #a0a0a9;
145
- --color-text-disabled-dark: #757580;
139
+ --color-text-primary: #1e2126;
140
+ /* Secondary text: field labels, descriptions. Light is moved DOWN to
141
+ L 0.52 — the old #737373 measured 4.54:1 on #fafafa but only 4.17:1 on
142
+ the frame (#f0f0f2) it actually sits on most, below AA. Now 4.85:1 on
143
+ the frame, 5.29:1 on surface-50, 5.52:1 on white. Dark is moved UP to
144
+ L 0.765, one value for the panel and the site, which had two (#a0a0a9
145
+ and #b4b8bd): 9.4:1 on the frame, 8.4:1 on a card. */
146
+ --color-text-secondary: #646971;
147
+ /* Disabled: deliberately not AA. 4.5:1 on the disabled field fill would
148
+ need a value DARKER than the enabled label, so the disabled state would
149
+ read as the emphasised one; WCAG 1.4.3 exempts inactive controls for
150
+ exactly this reason. It lands at ~3.4:1 on white — legible at a glance,
151
+ still unmistakably muted against its enabled neighbour. Dark disabled
152
+ is moved up to L 0.605, which clears 4.6:1 on a card (was 3.9:1). */
153
+ --color-text-disabled: #868c94;
154
+ --color-text-primary-dark: #f6f7f8;
155
+ --color-text-secondary-dark: #aeb3ba;
156
+ --color-text-disabled-dark: #7d828a;
146
157
 
147
158
  /* Surface Colors */
148
- --color-surface-50: #fafafa;
149
- --color-surface-100: #f5f5f5;
150
- --color-surface-200: #e5e5e5;
151
- --color-surface-300: #d4d4d4;
152
- --color-surface-400: #a3a3a3;
153
- /* Muted text: captions, nav descriptions, table headers, eyebrows.
154
- #737373 was tuned for light and measured 4.74:1 on white but only
155
- 4.43:1 on #000 and 4.18:1 on #0a0a0a, so every dark surface — the
156
- marketing site, the console, the admin panel in dark mode — carried a
157
- muted tier below AA. #797979 is the balance point: it is the single
158
- value where the white and #111111 curves cross, at 4.35:1 on each,
159
- and it clears 4.5:1 outright on the two backgrounds that carry most of
160
- this token (4.82:1 on #000, 4.55:1 on #0a0a0a). */
161
- --color-surface-500: #797979;
162
- --color-surface-600: #404040;
163
- --color-surface-700: #262626;
164
- --color-surface-800: #111111;
165
- --color-surface-900: #0a0a0a;
159
+ --color-surface-50: #f9fafb;
160
+ --color-surface-100: #f4f5f7;
161
+ --color-surface-200: #e3e5e9;
162
+ --color-surface-300: #d1d4d9;
163
+ --color-surface-400: #9ea4ab;
164
+ /* Muted text: captions, nav descriptions, table headers, eyebrows. One
165
+ value has to serve both themes, so it sits at the lightness where the
166
+ white and surface-800 contrast curves cross (L 0.576): 4.34:1 on white,
167
+ 4.36:1 on surface-800, and it clears 4.5:1 on the two backgrounds that
168
+ carry most of this token (4.84:1 on #000, 4.57:1 on surface-900). */
169
+ --color-surface-500: #747a81;
170
+ --color-surface-600: #3c4046;
171
+ --color-surface-700: #23262b;
172
+ --color-surface-800: #0f1114;
173
+ --color-surface-900: #090a0c;
166
174
  --color-surface-950: #000000;
167
175
 
168
176
  /* Surface Accent Colors */
169
177
  --color-surface-accent-50: #f8fafc;
170
- --color-surface-accent-100: #f1f5f9;
178
+ --color-surface-accent-100: #f2f5f9;
171
179
  --color-surface-accent-200: #e2e8f0;
172
- --color-surface-accent-300: #cbd5e1;
180
+ --color-surface-accent-300: #ccd5e1;
173
181
  --color-surface-accent-400: #94a3b8;
174
182
  --color-surface-accent-500: #64748b;
175
183
  --color-surface-accent-600: #475569;
176
184
  --color-surface-accent-700: #334155;
177
- --color-surface-accent-800: #1e293b;
178
- --color-surface-accent-900: #172033;
179
- --color-surface-accent-950: #0f172a;
185
+ --color-surface-accent-800: #1d293b;
186
+ --color-surface-accent-900: #152133;
187
+ --color-surface-accent-950: #0c182a;
188
+
189
+ /* The Graphite ramp at Tailwind's own lightness steps. Tailwind's zinc,
190
+ neutral, gray and stone palettes are re-pointed at it (see the inline
191
+ block below), so a `bg-zinc-900` written anywhere — a website demo, an
192
+ editor toolbar, a customer's own component — lands on the system's one
193
+ neutral hue instead of zinc's 286°. Slate is left alone: it already sits
194
+ on 258°. */
195
+ --color-graphite-50: #f9fafb;
196
+ --color-graphite-100: #f4f5f7;
197
+ --color-graphite-200: #e3e5e9;
198
+ --color-graphite-300: #d1d4d9;
199
+ --color-graphite-400: #9ca1a9;
200
+ --color-graphite-500: #6e747b;
201
+ --color-graphite-600: #4e5359;
202
+ --color-graphite-700: #3c4046;
203
+ --color-graphite-800: #23262b;
204
+ --color-graphite-900: #15171b;
205
+ --color-graphite-950: #090a0c;
180
206
  }
181
207
 
182
208
  /* The semantic surfaces as Tailwind colors. `inline` is load-bearing: it makes
@@ -185,6 +211,52 @@
185
211
  `@theme` the alias would be computed once on `:root` and stay light. The
186
212
  variables themselves are defined at the bottom of this file. */
187
213
  @theme inline {
214
+ /* Tailwind's grey palettes, re-pointed at Graphite (defined above). */
215
+ --color-zinc-50: var(--color-graphite-50);
216
+ --color-zinc-100: var(--color-graphite-100);
217
+ --color-zinc-200: var(--color-graphite-200);
218
+ --color-zinc-300: var(--color-graphite-300);
219
+ --color-zinc-400: var(--color-graphite-400);
220
+ --color-zinc-500: var(--color-graphite-500);
221
+ --color-zinc-600: var(--color-graphite-600);
222
+ --color-zinc-700: var(--color-graphite-700);
223
+ --color-zinc-800: var(--color-graphite-800);
224
+ --color-zinc-900: var(--color-graphite-900);
225
+ --color-zinc-950: var(--color-graphite-950);
226
+ --color-neutral-50: var(--color-graphite-50);
227
+ --color-neutral-100: var(--color-graphite-100);
228
+ --color-neutral-200: var(--color-graphite-200);
229
+ --color-neutral-300: var(--color-graphite-300);
230
+ --color-neutral-400: var(--color-graphite-400);
231
+ --color-neutral-500: var(--color-graphite-500);
232
+ --color-neutral-600: var(--color-graphite-600);
233
+ --color-neutral-700: var(--color-graphite-700);
234
+ --color-neutral-800: var(--color-graphite-800);
235
+ --color-neutral-900: var(--color-graphite-900);
236
+ --color-neutral-950: var(--color-graphite-950);
237
+ --color-gray-50: var(--color-graphite-50);
238
+ --color-gray-100: var(--color-graphite-100);
239
+ --color-gray-200: var(--color-graphite-200);
240
+ --color-gray-300: var(--color-graphite-300);
241
+ --color-gray-400: var(--color-graphite-400);
242
+ --color-gray-500: var(--color-graphite-500);
243
+ --color-gray-600: var(--color-graphite-600);
244
+ --color-gray-700: var(--color-graphite-700);
245
+ --color-gray-800: var(--color-graphite-800);
246
+ --color-gray-900: var(--color-graphite-900);
247
+ --color-gray-950: var(--color-graphite-950);
248
+ --color-stone-50: var(--color-graphite-50);
249
+ --color-stone-100: var(--color-graphite-100);
250
+ --color-stone-200: var(--color-graphite-200);
251
+ --color-stone-300: var(--color-graphite-300);
252
+ --color-stone-400: var(--color-graphite-400);
253
+ --color-stone-500: var(--color-graphite-500);
254
+ --color-stone-600: var(--color-graphite-600);
255
+ --color-stone-700: var(--color-graphite-700);
256
+ --color-stone-800: var(--color-graphite-800);
257
+ --color-stone-900: var(--color-graphite-900);
258
+ --color-stone-950: var(--color-graphite-950);
259
+
188
260
  --color-surface-frame: var(--surface-frame);
189
261
  --color-surface-sheet: var(--surface-sheet);
190
262
  --color-surface-card: var(--surface-card);
@@ -244,12 +316,12 @@
244
316
  `.dark` are the only switches. */
245
317
  :root,
246
318
  [data-theme="light"] {
247
- --surface-frame: #f0f0f2;
248
- --surface-sheet: #f9f9fa;
319
+ --surface-frame: #eff0f3;
320
+ --surface-sheet: #f8f9fa;
249
321
  --surface-card: #ffffff;
250
- --surface-card-hover: #f7f7f8;
251
- --surface-raised: #ececee;
252
- --surface-raised-hover: #e3e3e6;
322
+ --surface-card-hover: #f6f7f9;
323
+ --surface-raised: #eaecef;
324
+ --surface-raised-hover: #e1e4e7;
253
325
  --surface-lifted: #ffffff;
254
326
  --surface-field: rgb(0 0 0 / 0.035);
255
327
  --surface-field-hover: rgb(0 0 0 / 0.055);
@@ -258,26 +330,26 @@
258
330
  --surface-scrim: rgb(0 0 0 / 0.4);
259
331
  /* The one surface that goes DOWN the ladder on purpose: a code well, a
260
332
  query, a log tail, a connection string, recessed into its card. */
261
- --surface-well: #f5f5f5;
333
+ --surface-well: #f4f5f7;
262
334
  --hairline: rgb(0 0 0 / 0.08);
263
335
  --hairline-strong: rgb(0 0 0 / 0.13);
264
336
  }
265
337
 
266
338
  .dark,
267
339
  [data-theme="dark"] {
268
- --surface-frame: #0a0a0a;
269
- --surface-sheet: #131313;
270
- --surface-card: #181818;
271
- --surface-card-hover: #1d1d1d;
272
- --surface-raised: #242424;
273
- --surface-raised-hover: #2c2c2c;
274
- --surface-lifted: #2c2c2c;
340
+ --surface-frame: #090a0c;
341
+ --surface-sheet: #111316;
342
+ --surface-card: #16181c;
343
+ --surface-card-hover: #1b1d21;
344
+ --surface-raised: #212429;
345
+ --surface-raised-hover: #292c32;
346
+ --surface-lifted: #292c32;
275
347
  --surface-field: rgb(255 255 255 / 0.04);
276
348
  --surface-field-hover: rgb(255 255 255 / 0.07);
277
349
  --surface-hover: rgb(255 255 255 / 0.06);
278
350
  --surface-active: rgb(255 255 255 / 0.09);
279
351
  --surface-scrim: rgb(0 0 0 / 0.6);
280
- --surface-well: #0a0a0a;
352
+ --surface-well: #090a0c;
281
353
  --hairline: rgb(255 255 255 / 0.08);
282
354
  --hairline-strong: rgb(255 255 255 / 0.14);
283
355
  }
package/dist/styles.d.ts CHANGED
@@ -110,8 +110,8 @@ export declare const cardMixin = "bg-surface-card rounded-xl border border-hairl
110
110
  * An inset well: code, a query, a log tail, a connection string.
111
111
  *
112
112
  * It must read as *recessed into* the surface holding it, which means darker
113
- * than that surface in dark mode. Cards now sit at `surface-card` (#181818),
114
- * so #0a0a0a is already fourteen units below and reads as a well; #000 was
113
+ * than that surface in dark mode. Cards now sit at `surface-card` (#16181c),
114
+ * so #090a0c is already thirteen units below and reads as a well; #000 was
115
115
  * tuned for the old #0a card and is now harsher than it needs to be. That value
116
116
  * is the `surface-well` role (theme.css), the one surface that goes DOWN the
117
117
  * ladder on purpose. Pair with `font-mono`; this mixin carries the surface
package/dist/styles.es.js CHANGED
@@ -109,8 +109,8 @@ var cardMixin = "bg-surface-card rounded-xl border border-hairline";
109
109
  * An inset well: code, a query, a log tail, a connection string.
110
110
  *
111
111
  * It must read as *recessed into* the surface holding it, which means darker
112
- * than that surface in dark mode. Cards now sit at `surface-card` (#181818),
113
- * so #0a0a0a is already fourteen units below and reads as a well; #000 was
112
+ * than that surface in dark mode. Cards now sit at `surface-card` (#16181c),
113
+ * so #090a0c is already thirteen units below and reads as a well; #000 was
114
114
  * tuned for the old #0a card and is now harsher than it needs to be. That value
115
115
  * is the `surface-well` role (theme.css), the one surface that goes DOWN the
116
116
  * ladder on purpose. Pair with `font-mono`; this mixin carries the surface
@@ -1 +1 @@
1
- {"version":3,"file":"styles.es.js","names":[],"sources":["../src/styles.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Control size scale — the single source of truth for how tall an inline\n// control is.\n//\n// Every control that sits on a line with another control (buttons, text\n// fields, selects, search bars, date fields, icon buttons, checkboxes) resolves\n// its height from this map, so a Button and a TextField at the same `size` are\n// pixel-identical and share a baseline. Before this existed each component\n// carried its own map and `size=\"large\"` meant 42px on a Button and 64px on a\n// TextField.\n//\n// The scale is 28/32/40/48 — the rhythm IconButton and Checkbox already used.\n// `xl`/`2xl` continue above it and are button-only.\n// ---------------------------------------------------------------------------\n\nexport type ControlSize = \"smallest\" | \"small\" | \"medium\" | \"large\";\nexport type ButtonSize = ControlSize | \"xl\" | \"2xl\";\n\nexport const CONTROL_HEIGHT = {\n smallest: 28,\n small: 32,\n medium: 40,\n large: 48,\n xl: 56,\n \"2xl\": 64\n} as const satisfies Record<ButtonSize, number>;\n\n/** Tailwind min-height class per control size. Keep in sync with CONTROL_HEIGHT. */\nexport const controlHeightMixin = {\n smallest: \"min-h-[28px]\",\n small: \"min-h-[32px]\",\n medium: \"min-h-[40px]\",\n large: \"min-h-[48px]\",\n xl: \"min-h-[56px]\",\n \"2xl\": \"min-h-[64px]\"\n} as const satisfies Record<ButtonSize, string>;\n\n/** Horizontal padding paired with each size, so gutters scale with height. */\nexport const controlPaddingMixin = {\n smallest: \"px-2\",\n small: \"px-2\",\n medium: \"px-3\",\n large: \"px-4\",\n xl: \"px-6\",\n \"2xl\": \"px-10\"\n} as const satisfies Record<ButtonSize, string>;\n\n/**\n * Suppress the focus ring on a surface that is focusable but is not a control —\n * a popover panel, a menu container, a bare cell input inside a table that\n * indicates focus by other means.\n *\n * Still expressed as `ring-0`, and that is why the default ring in\n * `index.css` overrides `--tw-ring-shadow` rather than switching to\n * `inset-ring-*`: `inset-ring` is a different variable, which this would not\n * have silenced, and every surface that had opted out would have got a ring\n * back.\n */\nexport const focusedDisabled = \"focus-visible:ring-0 focus-visible:ring-offset-0\";\nexport const focusedInvisibleMixin = \"focus:bg-surface-field-hover\";\n\n/**\n * The focus ring, drawn OUTSIDE the element, for a control that opts into it.\n *\n * The product's default ring is the `:focus-visible` rule in `index.css`, and\n * that one is drawn *inside* the border box: an outset ring is paint beyond the\n * box, and any ancestor that clips — a scroller, a truncating cell, a\n * max-height collapse — cuts it. This one is still outset because its only\n * caller is a whole field row sitting inside `p-4`/`p-6` of card padding, where\n * there is room around it and nothing clipping within 16px. Reach for it only\n * where that is true; anywhere else, let the default apply.\n *\n * Three dead declarations were removed rather than reshuffled:\n * `outline-hidden` + `outline-none` — the same instruction twice.\n * `ring-primary` + `ring-primary/50` — the first could never win.\n * `ring-opacity-50` — Tailwind v3 syntax. v4 expresses\n * opacity with the `/50` slash form, so\n * this generated nothing at all.\n *\n * What is left is the ring that was actually rendering, at 60% rather than 50%:\n * on `surface-900` the old value sat close enough to the field's own border to\n * be missed at a glance, which is the one thing a focus ring may not be.\n * `ring-offset-0` + a transparent offset stay — without them the ring draws a\n * white halo on dark surfaces.\n */\nexport const focusedClasses = \"z-30 outline-none ring-2 ring-primary/60 ring-offset-0 ring-offset-transparent\";\n\n/**\n * Field surfaces.\n *\n * A field is the one surface that goes the other way: it is INSET into\n * whatever holds it, so `surface-field` is an alpha fill rather than a solid,\n * and it composes over a card, the sheet or a dialog alike. It also carries\n * the hairline: a field is an object (DESIGN.md rule 2), and without the line\n * a 4% fill on a card reads as a soft slab rather than a control — measured on\n * the product form, eight of them in a row looked like a wall. The reference\n * draws every input with its 1px edge, on both themes.\n *\n * The 1px inset top highlight that used to sit here was the recessed-edge\n * device from before the field had a border; with a real hairline above it\n * the two stacked into a doubled top edge. The border does its work now.\n *\n * Exactly one element carries this mixin per field — the box, never the\n * input inside it — so the alpha fill and its hover composite once.\n */\nexport const fieldBackgroundMixin = \"bg-surface-field border border-hairline\";\nexport const fieldBackgroundInvisibleMixin = \"bg-transparent\";\nexport const fieldBackgroundDisabledMixin = \"bg-surface-field/60\";\n\n/**\n * `transition-colors` here, because every other interactive surface in the kit\n * has it and fields did not — `cardClickableMixin` eases, a TextField snapped.\n * On a form of eight fields that difference is the whole feel of the screen.\n */\nexport const fieldBackgroundHoverMixin = \"transition-colors duration-150 hover:bg-surface-field-hover\";\n\n/**\n * The hairline. One value per theme (`--hairline` in theme.css), and where it\n * goes matters more than what it is: a line marks an OBJECT — a card on the\n * sheet, a field, a floating surface. It does not mark a region: a tile inside\n * a card, a row, a segmented track, a button, a chip are fills, and the\n * lightness step between them is the whole separation. One exception, and it\n * is the sheet's own edge against the frame: that step is about 3 L* on both\n * themes and did not read on a real screen, so the Scaffold draws this line\n * there. Measured before this existed, the panel drew the same line on every\n * card and every tile as well, and with only two surface values available the\n * line was doing the work a step should have done.\n */\nexport const defaultBorderMixin = \"border-hairline\";\n\n// ---------------------------------------------------------------------------\n// Surfaces are named by ROLE (see theme.css) and the theme decides the value.\n// Every nested surface is one step lifted from the surface it sits on, on both\n// themes. The rules are in packages/ui/DESIGN.md (Surfaces); the measurements\n// behind the ladder are in docs/design/instatic-distilled.html.\n//\n// Two kinds of card, and the difference is what the border is for.\n//\n// A **floating** surface — a menu, a dialog, a popover — sits OVER the page. It\n// has to be legible against whatever happens to be underneath it, so its edge\n// is definite: `hairline-strong`. That is `paperMixin`.\n//\n// A **page** surface — a card in the document flow — sits ON the sheet, and it\n// is already one step lighter than the sheet. It keeps a plain hairline, which\n// is all a card on the sheet needs; once the step is there the line is close\n// to optional, and a stronger one reads as a box drawn around content.\n// ---------------------------------------------------------------------------\nexport const paperMixin = \"bg-surface-card rounded-lg border border-hairline-strong\";\nexport const cardMixin = \"bg-surface-card rounded-xl border border-hairline\";\n\n/**\n * An inset well: code, a query, a log tail, a connection string.\n *\n * It must read as *recessed into* the surface holding it, which means darker\n * than that surface in dark mode. Cards now sit at `surface-card` (#181818),\n * so #0a0a0a is already fourteen units below and reads as a well; #000 was\n * tuned for the old #0a card and is now harsher than it needs to be. That value\n * is the `surface-well` role (theme.css), the one surface that goes DOWN the\n * ladder on purpose. Pair with `font-mono`; this mixin carries the surface\n * only.\n */\nexport const codeSurfaceMixin = \"bg-surface-well rounded-md\";\n\n/**\n * The accent, used as TEXT.\n *\n * `--color-primary` (#0070F4) is tuned to be a fill — white text on it, it on\n * white. Read as text on our dark surfaces it lands at **4.36:1 on a\n * `surface-900` card** (measured), which is below AA for body-sized text, and\n * every accent link in the product sits on exactly that surface. On the page's\n * `surface-950` it only reaches 4.62:1, so the margin was never real.\n *\n * `--color-primary-light` is the same hue lifted 0.15 in OKLCH lightness and\n * measures **7.34:1** on the same card. It is indistinguishable as \"the blue\"\n * and comfortably legible, so dark mode swaps to it for text.\n *\n * Use this for links, accent labels and any accent-coloured type. It is NOT for\n * fills — a filled button keeps `bg-primary`, where the contrast question runs\n * the other way and #0070F4 is already correct.\n */\nexport const accentTextMixin = \"text-primary dark:text-primary-light\";\n\n/**\n * Hover on a SOLID surface goes one step lighter, and it stays neutral. The\n * previous `hover:bg-primary/5` tinted every clickable card blue, which spent\n * the brand colour on a state that carries no meaning; on the dark ladder it\n * also read as a hue shift rather than a lift.\n */\nexport const cardClickableMixin = \"hover:bg-surface-card-hover cursor-pointer transition-colors duration-150\";\nexport const cardSelectedMixin = \"bg-primary-bg/30 dark:bg-primary-bg/10 ring-1 ring-primary/75\";\n\n/**\n * Hover on a TRANSPARENT ground — a nav item, a menu item, a list row, a tree\n * row. `surface-hover` is an alpha fill, so it lifts whatever it lands on by\n * the same amount, and `surface-active` is the same idea one step further for\n * the selected row when no hue is wanted. Do not use these on a solid card:\n * an alpha fill REPLACES a solid background rather than tinting it.\n */\nexport const surfaceHoverMixin = \"hover:bg-surface-hover transition-colors duration-150\";\nexport const surfaceActiveMixin = \"bg-surface-active\";\n"],"mappings":";AAkBA,IAAa,iBAAiB;CAC1B,UAAU;CACV,OAAO;CACP,QAAQ;CACR,OAAO;CACP,IAAI;CACJ,OAAO;AACX;;AAGA,IAAa,qBAAqB;CAC9B,UAAU;CACV,OAAO;CACP,QAAQ;CACR,OAAO;CACP,IAAI;CACJ,OAAO;AACX;;AAGA,IAAa,sBAAsB;CAC/B,UAAU;CACV,OAAO;CACP,QAAQ;CACR,OAAO;CACP,IAAI;CACJ,OAAO;AACX;;;;;;;;;;;;AAaA,IAAa,kBAAkB;AAC/B,IAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;AA0BrC,IAAa,iBAAiB;;;;;;;;;;;;;;;;;;;AAoB9B,IAAa,uBAAuB;AACpC,IAAa,gCAAgC;AAC7C,IAAa,+BAA+B;;;;;;AAO5C,IAAa,4BAA4B;;;;;;;;;;;;;AAczC,IAAa,qBAAqB;AAmBlC,IAAa,aAAa;AAC1B,IAAa,YAAY;;;;;;;;;;;;AAazB,IAAa,mBAAmB;;;;;;;;;;;;;;;;;;AAmBhC,IAAa,kBAAkB;;;;;;;AAQ/B,IAAa,qBAAqB;AAClC,IAAa,oBAAoB;;;;;;;;AASjC,IAAa,oBAAoB;AACjC,IAAa,qBAAqB"}
1
+ {"version":3,"file":"styles.es.js","names":[],"sources":["../src/styles.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// Control size scale — the single source of truth for how tall an inline\n// control is.\n//\n// Every control that sits on a line with another control (buttons, text\n// fields, selects, search bars, date fields, icon buttons, checkboxes) resolves\n// its height from this map, so a Button and a TextField at the same `size` are\n// pixel-identical and share a baseline. Before this existed each component\n// carried its own map and `size=\"large\"` meant 42px on a Button and 64px on a\n// TextField.\n//\n// The scale is 28/32/40/48 — the rhythm IconButton and Checkbox already used.\n// `xl`/`2xl` continue above it and are button-only.\n// ---------------------------------------------------------------------------\n\nexport type ControlSize = \"smallest\" | \"small\" | \"medium\" | \"large\";\nexport type ButtonSize = ControlSize | \"xl\" | \"2xl\";\n\nexport const CONTROL_HEIGHT = {\n smallest: 28,\n small: 32,\n medium: 40,\n large: 48,\n xl: 56,\n \"2xl\": 64\n} as const satisfies Record<ButtonSize, number>;\n\n/** Tailwind min-height class per control size. Keep in sync with CONTROL_HEIGHT. */\nexport const controlHeightMixin = {\n smallest: \"min-h-[28px]\",\n small: \"min-h-[32px]\",\n medium: \"min-h-[40px]\",\n large: \"min-h-[48px]\",\n xl: \"min-h-[56px]\",\n \"2xl\": \"min-h-[64px]\"\n} as const satisfies Record<ButtonSize, string>;\n\n/** Horizontal padding paired with each size, so gutters scale with height. */\nexport const controlPaddingMixin = {\n smallest: \"px-2\",\n small: \"px-2\",\n medium: \"px-3\",\n large: \"px-4\",\n xl: \"px-6\",\n \"2xl\": \"px-10\"\n} as const satisfies Record<ButtonSize, string>;\n\n/**\n * Suppress the focus ring on a surface that is focusable but is not a control —\n * a popover panel, a menu container, a bare cell input inside a table that\n * indicates focus by other means.\n *\n * Still expressed as `ring-0`, and that is why the default ring in\n * `index.css` overrides `--tw-ring-shadow` rather than switching to\n * `inset-ring-*`: `inset-ring` is a different variable, which this would not\n * have silenced, and every surface that had opted out would have got a ring\n * back.\n */\nexport const focusedDisabled = \"focus-visible:ring-0 focus-visible:ring-offset-0\";\nexport const focusedInvisibleMixin = \"focus:bg-surface-field-hover\";\n\n/**\n * The focus ring, drawn OUTSIDE the element, for a control that opts into it.\n *\n * The product's default ring is the `:focus-visible` rule in `index.css`, and\n * that one is drawn *inside* the border box: an outset ring is paint beyond the\n * box, and any ancestor that clips — a scroller, a truncating cell, a\n * max-height collapse — cuts it. This one is still outset because its only\n * caller is a whole field row sitting inside `p-4`/`p-6` of card padding, where\n * there is room around it and nothing clipping within 16px. Reach for it only\n * where that is true; anywhere else, let the default apply.\n *\n * Three dead declarations were removed rather than reshuffled:\n * `outline-hidden` + `outline-none` — the same instruction twice.\n * `ring-primary` + `ring-primary/50` — the first could never win.\n * `ring-opacity-50` — Tailwind v3 syntax. v4 expresses\n * opacity with the `/50` slash form, so\n * this generated nothing at all.\n *\n * What is left is the ring that was actually rendering, at 60% rather than 50%:\n * on `surface-900` the old value sat close enough to the field's own border to\n * be missed at a glance, which is the one thing a focus ring may not be.\n * `ring-offset-0` + a transparent offset stay — without them the ring draws a\n * white halo on dark surfaces.\n */\nexport const focusedClasses = \"z-30 outline-none ring-2 ring-primary/60 ring-offset-0 ring-offset-transparent\";\n\n/**\n * Field surfaces.\n *\n * A field is the one surface that goes the other way: it is INSET into\n * whatever holds it, so `surface-field` is an alpha fill rather than a solid,\n * and it composes over a card, the sheet or a dialog alike. It also carries\n * the hairline: a field is an object (DESIGN.md rule 2), and without the line\n * a 4% fill on a card reads as a soft slab rather than a control — measured on\n * the product form, eight of them in a row looked like a wall. The reference\n * draws every input with its 1px edge, on both themes.\n *\n * The 1px inset top highlight that used to sit here was the recessed-edge\n * device from before the field had a border; with a real hairline above it\n * the two stacked into a doubled top edge. The border does its work now.\n *\n * Exactly one element carries this mixin per field — the box, never the\n * input inside it — so the alpha fill and its hover composite once.\n */\nexport const fieldBackgroundMixin = \"bg-surface-field border border-hairline\";\nexport const fieldBackgroundInvisibleMixin = \"bg-transparent\";\nexport const fieldBackgroundDisabledMixin = \"bg-surface-field/60\";\n\n/**\n * `transition-colors` here, because every other interactive surface in the kit\n * has it and fields did not — `cardClickableMixin` eases, a TextField snapped.\n * On a form of eight fields that difference is the whole feel of the screen.\n */\nexport const fieldBackgroundHoverMixin = \"transition-colors duration-150 hover:bg-surface-field-hover\";\n\n/**\n * The hairline. One value per theme (`--hairline` in theme.css), and where it\n * goes matters more than what it is: a line marks an OBJECT — a card on the\n * sheet, a field, a floating surface. It does not mark a region: a tile inside\n * a card, a row, a segmented track, a button, a chip are fills, and the\n * lightness step between them is the whole separation. One exception, and it\n * is the sheet's own edge against the frame: that step is about 3 L* on both\n * themes and did not read on a real screen, so the Scaffold draws this line\n * there. Measured before this existed, the panel drew the same line on every\n * card and every tile as well, and with only two surface values available the\n * line was doing the work a step should have done.\n */\nexport const defaultBorderMixin = \"border-hairline\";\n\n// ---------------------------------------------------------------------------\n// Surfaces are named by ROLE (see theme.css) and the theme decides the value.\n// Every nested surface is one step lifted from the surface it sits on, on both\n// themes. The rules are in packages/ui/DESIGN.md (Surfaces); the measurements\n// behind the ladder are in docs/design/instatic-distilled.html.\n//\n// Two kinds of card, and the difference is what the border is for.\n//\n// A **floating** surface — a menu, a dialog, a popover — sits OVER the page. It\n// has to be legible against whatever happens to be underneath it, so its edge\n// is definite: `hairline-strong`. That is `paperMixin`.\n//\n// A **page** surface — a card in the document flow — sits ON the sheet, and it\n// is already one step lighter than the sheet. It keeps a plain hairline, which\n// is all a card on the sheet needs; once the step is there the line is close\n// to optional, and a stronger one reads as a box drawn around content.\n// ---------------------------------------------------------------------------\nexport const paperMixin = \"bg-surface-card rounded-lg border border-hairline-strong\";\nexport const cardMixin = \"bg-surface-card rounded-xl border border-hairline\";\n\n/**\n * An inset well: code, a query, a log tail, a connection string.\n *\n * It must read as *recessed into* the surface holding it, which means darker\n * than that surface in dark mode. Cards now sit at `surface-card` (#16181c),\n * so #090a0c is already thirteen units below and reads as a well; #000 was\n * tuned for the old #0a card and is now harsher than it needs to be. That value\n * is the `surface-well` role (theme.css), the one surface that goes DOWN the\n * ladder on purpose. Pair with `font-mono`; this mixin carries the surface\n * only.\n */\nexport const codeSurfaceMixin = \"bg-surface-well rounded-md\";\n\n/**\n * The accent, used as TEXT.\n *\n * `--color-primary` (#0070F4) is tuned to be a fill — white text on it, it on\n * white. Read as text on our dark surfaces it lands at **4.36:1 on a\n * `surface-900` card** (measured), which is below AA for body-sized text, and\n * every accent link in the product sits on exactly that surface. On the page's\n * `surface-950` it only reaches 4.62:1, so the margin was never real.\n *\n * `--color-primary-light` is the same hue lifted 0.15 in OKLCH lightness and\n * measures **7.34:1** on the same card. It is indistinguishable as \"the blue\"\n * and comfortably legible, so dark mode swaps to it for text.\n *\n * Use this for links, accent labels and any accent-coloured type. It is NOT for\n * fills — a filled button keeps `bg-primary`, where the contrast question runs\n * the other way and #0070F4 is already correct.\n */\nexport const accentTextMixin = \"text-primary dark:text-primary-light\";\n\n/**\n * Hover on a SOLID surface goes one step lighter, and it stays neutral. The\n * previous `hover:bg-primary/5` tinted every clickable card blue, which spent\n * the brand colour on a state that carries no meaning; on the dark ladder it\n * also read as a hue shift rather than a lift.\n */\nexport const cardClickableMixin = \"hover:bg-surface-card-hover cursor-pointer transition-colors duration-150\";\nexport const cardSelectedMixin = \"bg-primary-bg/30 dark:bg-primary-bg/10 ring-1 ring-primary/75\";\n\n/**\n * Hover on a TRANSPARENT ground — a nav item, a menu item, a list row, a tree\n * row. `surface-hover` is an alpha fill, so it lifts whatever it lands on by\n * the same amount, and `surface-active` is the same idea one step further for\n * the selected row when no hue is wanted. Do not use these on a solid card:\n * an alpha fill REPLACES a solid background rather than tinting it.\n */\nexport const surfaceHoverMixin = \"hover:bg-surface-hover transition-colors duration-150\";\nexport const surfaceActiveMixin = \"bg-surface-active\";\n"],"mappings":";AAkBA,IAAa,iBAAiB;CAC1B,UAAU;CACV,OAAO;CACP,QAAQ;CACR,OAAO;CACP,IAAI;CACJ,OAAO;AACX;;AAGA,IAAa,qBAAqB;CAC9B,UAAU;CACV,OAAO;CACP,QAAQ;CACR,OAAO;CACP,IAAI;CACJ,OAAO;AACX;;AAGA,IAAa,sBAAsB;CAC/B,UAAU;CACV,OAAO;CACP,QAAQ;CACR,OAAO;CACP,IAAI;CACJ,OAAO;AACX;;;;;;;;;;;;AAaA,IAAa,kBAAkB;AAC/B,IAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;AA0BrC,IAAa,iBAAiB;;;;;;;;;;;;;;;;;;;AAoB9B,IAAa,uBAAuB;AACpC,IAAa,gCAAgC;AAC7C,IAAa,+BAA+B;;;;;;AAO5C,IAAa,4BAA4B;;;;;;;;;;;;;AAczC,IAAa,qBAAqB;AAmBlC,IAAa,aAAa;AAC1B,IAAa,YAAY;;;;;;;;;;;;AAazB,IAAa,mBAAmB;;;;;;;;;;;;;;;;;;AAmBhC,IAAa,kBAAkB;;;;;;;AAQ/B,IAAa,qBAAqB;AAClC,IAAa,oBAAoB;;;;;;;;AASjC,IAAa,oBAAoB;AACjC,IAAa,qBAAqB"}
package/dist/theme.css CHANGED
@@ -115,68 +115,94 @@
115
115
  --color-primary-bg: oklch(from var(--color-primary) l c h / 0.1);
116
116
  --color-secondary-bg: oklch(from var(--color-secondary) l c h / 0.1);
117
117
 
118
+ /* GRAPHITE — every neutral in this file, text and surface, sits on ONE hue:
119
+ 258°, the brand blue's own, at a chroma low enough (0.002-0.014) that
120
+ nothing reads as blue. Adopted 2026-10-14.
121
+
122
+ Before, the neutrals sat on five hues: the surface ladder had none (pure
123
+ grey), the text greys were Tailwind's zinc at 286° (violet), the site and
124
+ film grounds 246°, the slate scale 264°. A hue-less grey beside a cool
125
+ black reads warm and muddy, so a card looked like a grey box set on
126
+ black rather than a lifted piece of the same surface.
127
+
128
+ Each value is `oklch(L c 258)` with the lightness it had before, so
129
+ contrast and the ladder's steps did not move; the exceptions are the
130
+ text tiers noted below, moved on purpose. The values are written as hex
131
+ because tokens are read by more than CSS (canvas, email, charts), and a
132
+ guard (test/neutral-hue.test.ts) fails on any neutral off the hue. */
133
+
118
134
  /* Field Colors */
119
- --color-field-disabled: rgb(224 224 226);
120
- --color-field-disabled-dark: rgb(35 35 37);
135
+ --color-field-disabled: #e1e4e7;
136
+ --color-field-disabled-dark: #212429;
121
137
 
122
138
  /* Text Colors — using opaque values so icon strokes render solidly */
123
- --color-text-primary: #212121;
124
- /* #737373, not #757575. Two units, and they are the two that matter.
125
- The app's light page ground is #fafafa, not #ffffff, and #757575 lands at
126
- 4.41:1 there — under AA for the 14px type this token carries. It clears
127
- 4.5 only against pure white, which is not a surface the product actually
128
- uses. Every field label in light mode was failing by a whisker.
129
- #737373 measures 4.54:1 on #fafafa and 4.74:1 on #ffffff, so it passes on
130
- both grounds instead of the theoretical one. Same fix, and the same
131
- reasoning, as the `--color-surface-500` move recorded below. */
132
- --color-text-secondary: #737373;
133
- /* #8b8b8b, not #9e9e9e. Measured against the field fill a disabled label
134
- actually sits on (`surface-accent-200/50` over #fafafa ≈ #eef1f5),
135
- #9e9e9e reached 2.36:1 — present, but not readable.
136
- Full AA is not achievable here and should not be chased: 4.5:1 on that
137
- fill needs #6e6e6e, which is DARKER than the #737373 an ENABLED label
138
- uses, so the disabled state would read as the emphasised one. WCAG 1.4.3
139
- exempts inactive controls for exactly this reason.
140
- #8b8b8b lands at ~3:1 — legible at a glance, still unmistakably muted
141
- against its enabled neighbour. */
142
- --color-text-disabled: #8b8b8b;
143
- --color-text-primary-dark: #ffffff;
144
- --color-text-secondary-dark: #a0a0a9;
145
- --color-text-disabled-dark: #757580;
139
+ --color-text-primary: #1e2126;
140
+ /* Secondary text: field labels, descriptions. Light is moved DOWN to
141
+ L 0.52 — the old #737373 measured 4.54:1 on #fafafa but only 4.17:1 on
142
+ the frame (#f0f0f2) it actually sits on most, below AA. Now 4.85:1 on
143
+ the frame, 5.29:1 on surface-50, 5.52:1 on white. Dark is moved UP to
144
+ L 0.765, one value for the panel and the site, which had two (#a0a0a9
145
+ and #b4b8bd): 9.4:1 on the frame, 8.4:1 on a card. */
146
+ --color-text-secondary: #646971;
147
+ /* Disabled: deliberately not AA. 4.5:1 on the disabled field fill would
148
+ need a value DARKER than the enabled label, so the disabled state would
149
+ read as the emphasised one; WCAG 1.4.3 exempts inactive controls for
150
+ exactly this reason. It lands at ~3.4:1 on white — legible at a glance,
151
+ still unmistakably muted against its enabled neighbour. Dark disabled
152
+ is moved up to L 0.605, which clears 4.6:1 on a card (was 3.9:1). */
153
+ --color-text-disabled: #868c94;
154
+ --color-text-primary-dark: #f6f7f8;
155
+ --color-text-secondary-dark: #aeb3ba;
156
+ --color-text-disabled-dark: #7d828a;
146
157
 
147
158
  /* Surface Colors */
148
- --color-surface-50: #fafafa;
149
- --color-surface-100: #f5f5f5;
150
- --color-surface-200: #e5e5e5;
151
- --color-surface-300: #d4d4d4;
152
- --color-surface-400: #a3a3a3;
153
- /* Muted text: captions, nav descriptions, table headers, eyebrows.
154
- #737373 was tuned for light and measured 4.74:1 on white but only
155
- 4.43:1 on #000 and 4.18:1 on #0a0a0a, so every dark surface — the
156
- marketing site, the console, the admin panel in dark mode — carried a
157
- muted tier below AA. #797979 is the balance point: it is the single
158
- value where the white and #111111 curves cross, at 4.35:1 on each,
159
- and it clears 4.5:1 outright on the two backgrounds that carry most of
160
- this token (4.82:1 on #000, 4.55:1 on #0a0a0a). */
161
- --color-surface-500: #797979;
162
- --color-surface-600: #404040;
163
- --color-surface-700: #262626;
164
- --color-surface-800: #111111;
165
- --color-surface-900: #0a0a0a;
159
+ --color-surface-50: #f9fafb;
160
+ --color-surface-100: #f4f5f7;
161
+ --color-surface-200: #e3e5e9;
162
+ --color-surface-300: #d1d4d9;
163
+ --color-surface-400: #9ea4ab;
164
+ /* Muted text: captions, nav descriptions, table headers, eyebrows. One
165
+ value has to serve both themes, so it sits at the lightness where the
166
+ white and surface-800 contrast curves cross (L 0.576): 4.34:1 on white,
167
+ 4.36:1 on surface-800, and it clears 4.5:1 on the two backgrounds that
168
+ carry most of this token (4.84:1 on #000, 4.57:1 on surface-900). */
169
+ --color-surface-500: #747a81;
170
+ --color-surface-600: #3c4046;
171
+ --color-surface-700: #23262b;
172
+ --color-surface-800: #0f1114;
173
+ --color-surface-900: #090a0c;
166
174
  --color-surface-950: #000000;
167
175
 
168
176
  /* Surface Accent Colors */
169
177
  --color-surface-accent-50: #f8fafc;
170
- --color-surface-accent-100: #f1f5f9;
178
+ --color-surface-accent-100: #f2f5f9;
171
179
  --color-surface-accent-200: #e2e8f0;
172
- --color-surface-accent-300: #cbd5e1;
180
+ --color-surface-accent-300: #ccd5e1;
173
181
  --color-surface-accent-400: #94a3b8;
174
182
  --color-surface-accent-500: #64748b;
175
183
  --color-surface-accent-600: #475569;
176
184
  --color-surface-accent-700: #334155;
177
- --color-surface-accent-800: #1e293b;
178
- --color-surface-accent-900: #172033;
179
- --color-surface-accent-950: #0f172a;
185
+ --color-surface-accent-800: #1d293b;
186
+ --color-surface-accent-900: #152133;
187
+ --color-surface-accent-950: #0c182a;
188
+
189
+ /* The Graphite ramp at Tailwind's own lightness steps. Tailwind's zinc,
190
+ neutral, gray and stone palettes are re-pointed at it (see the inline
191
+ block below), so a `bg-zinc-900` written anywhere — a website demo, an
192
+ editor toolbar, a customer's own component — lands on the system's one
193
+ neutral hue instead of zinc's 286°. Slate is left alone: it already sits
194
+ on 258°. */
195
+ --color-graphite-50: #f9fafb;
196
+ --color-graphite-100: #f4f5f7;
197
+ --color-graphite-200: #e3e5e9;
198
+ --color-graphite-300: #d1d4d9;
199
+ --color-graphite-400: #9ca1a9;
200
+ --color-graphite-500: #6e747b;
201
+ --color-graphite-600: #4e5359;
202
+ --color-graphite-700: #3c4046;
203
+ --color-graphite-800: #23262b;
204
+ --color-graphite-900: #15171b;
205
+ --color-graphite-950: #090a0c;
180
206
  }
181
207
 
182
208
  /* The semantic surfaces as Tailwind colors. `inline` is load-bearing: it makes
@@ -185,6 +211,52 @@
185
211
  `@theme` the alias would be computed once on `:root` and stay light. The
186
212
  variables themselves are defined at the bottom of this file. */
187
213
  @theme inline {
214
+ /* Tailwind's grey palettes, re-pointed at Graphite (defined above). */
215
+ --color-zinc-50: var(--color-graphite-50);
216
+ --color-zinc-100: var(--color-graphite-100);
217
+ --color-zinc-200: var(--color-graphite-200);
218
+ --color-zinc-300: var(--color-graphite-300);
219
+ --color-zinc-400: var(--color-graphite-400);
220
+ --color-zinc-500: var(--color-graphite-500);
221
+ --color-zinc-600: var(--color-graphite-600);
222
+ --color-zinc-700: var(--color-graphite-700);
223
+ --color-zinc-800: var(--color-graphite-800);
224
+ --color-zinc-900: var(--color-graphite-900);
225
+ --color-zinc-950: var(--color-graphite-950);
226
+ --color-neutral-50: var(--color-graphite-50);
227
+ --color-neutral-100: var(--color-graphite-100);
228
+ --color-neutral-200: var(--color-graphite-200);
229
+ --color-neutral-300: var(--color-graphite-300);
230
+ --color-neutral-400: var(--color-graphite-400);
231
+ --color-neutral-500: var(--color-graphite-500);
232
+ --color-neutral-600: var(--color-graphite-600);
233
+ --color-neutral-700: var(--color-graphite-700);
234
+ --color-neutral-800: var(--color-graphite-800);
235
+ --color-neutral-900: var(--color-graphite-900);
236
+ --color-neutral-950: var(--color-graphite-950);
237
+ --color-gray-50: var(--color-graphite-50);
238
+ --color-gray-100: var(--color-graphite-100);
239
+ --color-gray-200: var(--color-graphite-200);
240
+ --color-gray-300: var(--color-graphite-300);
241
+ --color-gray-400: var(--color-graphite-400);
242
+ --color-gray-500: var(--color-graphite-500);
243
+ --color-gray-600: var(--color-graphite-600);
244
+ --color-gray-700: var(--color-graphite-700);
245
+ --color-gray-800: var(--color-graphite-800);
246
+ --color-gray-900: var(--color-graphite-900);
247
+ --color-gray-950: var(--color-graphite-950);
248
+ --color-stone-50: var(--color-graphite-50);
249
+ --color-stone-100: var(--color-graphite-100);
250
+ --color-stone-200: var(--color-graphite-200);
251
+ --color-stone-300: var(--color-graphite-300);
252
+ --color-stone-400: var(--color-graphite-400);
253
+ --color-stone-500: var(--color-graphite-500);
254
+ --color-stone-600: var(--color-graphite-600);
255
+ --color-stone-700: var(--color-graphite-700);
256
+ --color-stone-800: var(--color-graphite-800);
257
+ --color-stone-900: var(--color-graphite-900);
258
+ --color-stone-950: var(--color-graphite-950);
259
+
188
260
  --color-surface-frame: var(--surface-frame);
189
261
  --color-surface-sheet: var(--surface-sheet);
190
262
  --color-surface-card: var(--surface-card);
@@ -244,12 +316,12 @@
244
316
  `.dark` are the only switches. */
245
317
  :root,
246
318
  [data-theme="light"] {
247
- --surface-frame: #f0f0f2;
248
- --surface-sheet: #f9f9fa;
319
+ --surface-frame: #eff0f3;
320
+ --surface-sheet: #f8f9fa;
249
321
  --surface-card: #ffffff;
250
- --surface-card-hover: #f7f7f8;
251
- --surface-raised: #ececee;
252
- --surface-raised-hover: #e3e3e6;
322
+ --surface-card-hover: #f6f7f9;
323
+ --surface-raised: #eaecef;
324
+ --surface-raised-hover: #e1e4e7;
253
325
  --surface-lifted: #ffffff;
254
326
  --surface-field: rgb(0 0 0 / 0.035);
255
327
  --surface-field-hover: rgb(0 0 0 / 0.055);
@@ -258,26 +330,26 @@
258
330
  --surface-scrim: rgb(0 0 0 / 0.4);
259
331
  /* The one surface that goes DOWN the ladder on purpose: a code well, a
260
332
  query, a log tail, a connection string, recessed into its card. */
261
- --surface-well: #f5f5f5;
333
+ --surface-well: #f4f5f7;
262
334
  --hairline: rgb(0 0 0 / 0.08);
263
335
  --hairline-strong: rgb(0 0 0 / 0.13);
264
336
  }
265
337
 
266
338
  .dark,
267
339
  [data-theme="dark"] {
268
- --surface-frame: #0a0a0a;
269
- --surface-sheet: #131313;
270
- --surface-card: #181818;
271
- --surface-card-hover: #1d1d1d;
272
- --surface-raised: #242424;
273
- --surface-raised-hover: #2c2c2c;
274
- --surface-lifted: #2c2c2c;
340
+ --surface-frame: #090a0c;
341
+ --surface-sheet: #111316;
342
+ --surface-card: #16181c;
343
+ --surface-card-hover: #1b1d21;
344
+ --surface-raised: #212429;
345
+ --surface-raised-hover: #292c32;
346
+ --surface-lifted: #292c32;
275
347
  --surface-field: rgb(255 255 255 / 0.04);
276
348
  --surface-field-hover: rgb(255 255 255 / 0.07);
277
349
  --surface-hover: rgb(255 255 255 / 0.06);
278
350
  --surface-active: rgb(255 255 255 / 0.09);
279
351
  --surface-scrim: rgb(0 0 0 / 0.6);
280
- --surface-well: #0a0a0a;
352
+ --surface-well: #090a0c;
281
353
  --hairline: rgb(255 255 255 / 0.08);
282
354
  --hairline-strong: rgb(255 255 255 / 0.14);
283
355
  }
@@ -4,7 +4,7 @@ import { hashString } from "./hash.es.js";
4
4
  var CONTRAST_TARGET = 4.6;
5
5
  /** Page backgrounds the outlined variant sits on: `bg-white` and `surface-950`. */
6
6
  var PAGE_LIGHT = "#ffffff";
7
- var PAGE_DARK = "#181818";
7
+ var PAGE_DARK = "#16181c";
8
8
  function toRgb(hex) {
9
9
  let h = hex.replace("#", "");
10
10
  if (h.length === 3) h = h.split("").map((c) => c + c).join("");
@@ -169,11 +169,11 @@ var HUE_STOPS = {
169
169
  text: "#280b42"
170
170
  },
171
171
  gray: {
172
- pale: "#eeeeee",
173
- mid: "#cccccc",
174
- solid: "#666666",
175
- deep: "#444444",
176
- text: "#040404"
172
+ pale: "#eceef1",
173
+ mid: "#c9ccd2",
174
+ solid: "#62676e",
175
+ deep: "#40444a",
176
+ text: "#040405"
177
177
  },
178
178
  indigo: {
179
179
  pale: "#e0e7ff",
@@ -1 +1 @@
1
- {"version":3,"file":"chip_colors.es.js","names":[],"sources":["../../src/util/chip_colors.ts"],"sourcesContent":["import { hashString } from \"./hash\";\n\nexport type ChipColorScheme = {\n color: string;\n text: string;\n /** Background color override for dark mode */\n darkColor?: string;\n /** Text color override for dark mode */\n darkText?: string;\n /**\n * Ink for the `outlined` variant, which has no fill of its own.\n *\n * `text`/`darkText` are the ink ON the chip's own background. An outlined\n * chip drops that background and sits on the PAGE, so the same value is\n * being asked to be legible against two different surfaces at once — and\n * for most hues it cannot be. Splitting the roles is what lets the filled\n * ink follow its fill (often dark ink on a bright chip) without dragging\n * the outlined variant down with it.\n */\n outlineText?: string;\n /** Outlined-variant ink in dark mode. */\n darkOutlineText?: string;\n /**\n * The `tinted` variant: the hue's solid stop at low alpha, as an `rgba()`\n * string so it composes over a card, the sheet or a dialog alike, and an\n * ink MEASURED on what that tint reads as over the page. A tint is the\n * reference's rule for hue in the chrome — a 12–16% wash behind hue-coloured\n * text — and it is what a status or enum chip paints by default now; the\n * saturated `color` stop is the `filled` variant, kept for swatches.\n */\n tintColor?: string;\n tintText?: string;\n darkTintColor?: string;\n darkTintText?: string;\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Contrast\n ────────────────────────────────────────────────────────────────────────────\n\n Chip ink is DERIVED, not hand-picked. It used to be written down per tone —\n `\"#fff\"` on every `solid` background, the hue's `pale` on every `deep` one —\n and an audit of all 120 hue/tone/mode pairs found **63 of them below WCAG AA**.\n The worst was white on `teal.solid` at **1.76:1**, which is not a near miss;\n it is unreadable. The palette is an Airtable-style one whose mid stops are\n bright enough to need dark ink, and hardcoding light ink ignored that.\n\n So the ink is measured against the background it will actually sit on, and\n pushed toward black or white only as far as it must go to clear the floor.\n Starting from the hue's own dark/light tints rather than from flat `#000`\n and `#fff` keeps the family looking like a family: `blue.solid` gets a very\n dark navy, not black.\n\n Deriving it also means a new hue cannot be added below AA — there is no\n per-tone ink to forget to check. */\n\n/** WCAG floor, plus a little margin so rounding cannot drop a pair below 4.5. */\nconst CONTRAST_TARGET = 4.6;\n\n/** Page backgrounds the outlined variant sits on: `bg-white` and `surface-950`. */\nconst PAGE_LIGHT = \"#ffffff\";\nconst PAGE_DARK = \"#181818\";\n\nfunction toRgb(hex: string): [number, number, number] {\n let h = hex.replace(\"#\", \"\");\n if (h.length === 3) h = h.split(\"\").map(c => c + c).join(\"\");\n return [0, 2, 4].map(i => parseInt(h.slice(i, i + 2), 16)) as [number, number, number];\n}\n\nfunction toHex(rgb: number[]): string {\n return \"#\" + rgb.map(v => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nfunction toRgba(hex: string, alpha: number): string {\n const [r, g, b] = toRgb(hex);\n return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\n/**\n * What a translucent colour reads as over an opaque one: a per-channel sRGB\n * mix, which is how the browser composites it. The tint ink is pushed against\n * this, never against the page alone — a 16% wash of the hue moves the\n * background a long way toward the ink, and an ink that only cleared AA on\n * the bare page lands near 3.5:1 on its own tint.\n */\nexport function compositeOver(fgHex: string, alpha: number, bgHex: string): string {\n const fg = toRgb(fgHex);\n const bg = toRgb(bgHex);\n return toHex(fg.map((c, i) => alpha * c + (1 - alpha) * bg[i]));\n}\n\n/** Read an `rgba(r, g, b, a)` string back as what it composites to over `bgHex`. */\nexport function tintOnPage(rgba: string, bgHex: string): string {\n const m = rgba.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)(?:,\\s*([\\d.]+))?\\)/);\n if (!m) return rgba;\n const fg = toHex([Number(m[1]), Number(m[2]), Number(m[3])]);\n return compositeOver(fg, m[4] === undefined ? 1 : Number(m[4]), bgHex);\n}\n\n/**\n * Tint alpha by tone, light then dark. The tones keep meaning something in the\n * tinted variant — a `Darker` chip is a heavier wash, not a different hue —\n * and dark mode runs four points higher because the same alpha of a bright\n * hue reads fainter over near-black than over white.\n */\nconst TINT_ALPHA: Record<ChipTone, [light: number, dark: number]> = {\n Lighter: [0.10, 0.14],\n Light: [0.14, 0.18],\n Dark: [0.18, 0.22],\n Darker: [0.22, 0.26]\n};\n\nfunction relativeLuminance(hex: string): number {\n const [r, g, b] = toRgb(hex).map(v => {\n const c = v / 255;\n return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);\n });\n return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n}\n\nexport function contrastRatio(a: string, b: string): number {\n const x = relativeLuminance(a);\n const y = relativeLuminance(b);\n const [hi, lo] = x > y ? [x, y] : [y, x];\n return (hi + 0.05) / (lo + 0.05);\n}\n\nfunction mixHex(from: string, to: string, t: number): string {\n const A = toRgb(from);\n const B = toRgb(to);\n return toHex([0, 1, 2].map(i => A[i] + (B[i] - A[i]) * t));\n}\n\n/**\n * Walk a tinted ink toward black or white until it clears the floor on `bg`.\n *\n * Stops at the first passing step rather than going all the way, so the ink\n * keeps as much of its hue as legibility allows.\n */\nfunction push(base: string, toward: string, bg: string): string {\n for (let t = 0; t <= 1.0001; t += 0.02) {\n const candidate = mixHex(base, toward, t);\n if (contrastRatio(candidate, bg) >= CONTRAST_TARGET) return candidate;\n }\n return toward;\n}\n\n/** The ink for a chip filled with `bg`: whichever direction has more headroom. */\nfunction inkOn(stops: HueStops, bg: string): string {\n const dark = push(stops.text, \"#000000\", bg);\n const light = push(stops.onDeep ?? stops.pale, \"#ffffff\", bg);\n return contrastRatio(dark, bg) >= contrastRatio(light, bg) ? dark : light;\n}\n\n/**\n * The hues a chip can take, and the four stops each one is built from.\n *\n * `pale` and `mid` are backgrounds for dark text; `solid` and `deep` are\n * backgrounds for light text. `text` is the dark ink used on the two pale\n * stops, and `onDeep` the light ink on `deep` — it defaults to `pale`, and is\n * only set where a hue needs a tint of its own to stay legible.\n */\ntype HueStops = {\n pale: string;\n mid: string;\n solid: string;\n deep: string;\n text: string;\n onDeep?: string;\n}\n\nexport const CHIP_HUES = [\n \"blue\", \"cyan\", \"teal\", \"green\", \"yellow\", \"orange\", \"red\", \"pink\", \"purple\", \"gray\",\n \"indigo\", \"violet\", \"fuchsia\", \"rose\", \"emerald\"\n] as const;\n\nexport type ChipHue = typeof CHIP_HUES[number];\n\n/**\n * How light or saturated a chip of a given hue is.\n *\n * A hue on its own (`\"blue\"`) means `Lighter` — the tone the picker offers and\n * the one seeded chips used to be limited to.\n */\nexport type ChipTone = \"Lighter\" | \"Light\" | \"Dark\" | \"Darker\";\n\nexport type ChipColorKey = ChipHue | `${ChipHue}${ChipTone}`;\n\nconst HUE_STOPS: Record<ChipHue, HueStops> = {\n blue: { pale: \"#cfdfff\", mid: \"#9cc7ff\", solid: \"#2d7ff9\", deep: \"#2750ae\", text: \"#102046\" },\n cyan: { pale: \"#d0f0fd\", mid: \"#77d1f3\", solid: \"#18bfff\", deep: \"#0b76b7\", text: \"#04283f\" },\n teal: { pale: \"#c2f5e9\", mid: \"#72ddc3\", solid: \"#20d9d2\", deep: \"#06a09b\", text: \"#012524\", onDeep: \"#daf3e9\" },\n green: { pale: \"#d1f7c4\", mid: \"#93e088\", solid: \"#20c933\", deep: \"#338a17\", text: \"#0b1d05\" },\n yellow: { pale: \"#ffeab6\", mid: \"#ffd66e\", solid: \"#fcb400\", deep: \"#b87503\", text: \"#3b2501\" },\n orange: { pale: \"#fee2d5\", mid: \"#ffa981\", solid: \"#ff6f2c\", deep: \"#d74d26\", text: \"#6b2613\" },\n red: { pale: \"#ffdce5\", mid: \"#ff9eb7\", solid: \"#f82b60\", deep: \"#ba1e45\", text: \"#4c0c1c\" },\n pink: { pale: \"#ffdaf6\", mid: \"#f99de2\", solid: \"#ff08c2\", deep: \"#b2158b\", text: \"#400832\" },\n purple: { pale: \"#ede2fe\", mid: \"#cdb0ff\", solid: \"#8b46ff\", deep: \"#6b1cb0\", text: \"#280b42\" },\n gray: { pale: \"#eeeeee\", mid: \"#cccccc\", solid: \"#666666\", deep: \"#444444\", text: \"#040404\" },\n indigo: { pale: \"#e0e7ff\", mid: \"#a5b4fc\", solid: \"#6366f1\", deep: \"#4f46e5\", text: \"#312e81\" },\n violet: { pale: \"#ede9fe\", mid: \"#c4b5fd\", solid: \"#8b5cf6\", deep: \"#7c3aed\", text: \"#4c1d95\" },\n fuchsia: { pale: \"#fae8ff\", mid: \"#f0abfc\", solid: \"#d946ef\", deep: \"#c026d3\", text: \"#701a75\" },\n rose: { pale: \"#ffe4e6\", mid: \"#fda4af\", solid: \"#f43f5e\", deep: \"#e11d48\", text: \"#881337\" },\n emerald: { pale: \"#d1fae5\", mid: \"#6ee7b7\", solid: \"#10b981\", deep: \"#059669\", text: \"#064e3b\" }\n};\n\n/**\n * One tone of one hue.\n *\n * Backgrounds are the palette's, unchanged — the stops were not the problem and\n * moving them would have restyled every chip in the product. Only the ink moved,\n * and it moved to wherever the measurement says it has to be.\n */\nfunction tone(stops: HueStops, chipTone: ChipTone): ChipColorScheme {\n // The outlined variant is a property of the HUE, not of the tone: it has no\n // fill, so every tone of a hue sits on the same page background and wants\n // the same ink.\n const outline = {\n outlineText: push(stops.text, \"#000000\", PAGE_LIGHT),\n darkOutlineText: push(stops.onDeep ?? stops.pale, \"#ffffff\", PAGE_DARK)\n };\n\n // The tint is the SOLID stop washed out, on both themes, so a hue keeps\n // its identity across the two — a pale-stop tint of blue reads as grey.\n // Light ink starts from `deep` and dark ink from `mid`: the saturated end\n // that already sits on the right side of the composite, pushed only as\n // far as the measurement says.\n const [alphaLight, alphaDark] = TINT_ALPHA[chipTone];\n const tint = {\n tintColor: toRgba(stops.solid, alphaLight),\n tintText: push(stops.deep, \"#000000\", compositeOver(stops.solid, alphaLight, PAGE_LIGHT)),\n darkTintColor: toRgba(stops.solid, alphaDark),\n darkTintText: push(stops.mid, \"#ffffff\", compositeOver(stops.solid, alphaDark, PAGE_DARK))\n };\n\n const filled = (light: string, dark: string): ChipColorScheme => ({\n color: light,\n text: inkOn(stops, light),\n darkColor: dark,\n darkText: inkOn(stops, dark),\n ...outline,\n ...tint\n });\n\n switch (chipTone) {\n case \"Light\":\n return filled(stops.mid, stops.solid);\n case \"Dark\":\n return filled(stops.solid, stops.solid);\n case \"Darker\":\n return filled(stops.deep, stops.deep);\n case \"Lighter\":\n default:\n return filled(stops.pale, stops.deep);\n }\n}\n\nconst CHIP_TONES: ChipTone[] = [\"Lighter\", \"Light\", \"Dark\", \"Darker\"];\n\nfunction buildChipColors(): Record<ChipColorKey, ChipColorScheme> {\n const colors = {} as Record<ChipColorKey, ChipColorScheme>;\n for (const hue of CHIP_HUES) {\n const stops = HUE_STOPS[hue];\n for (const chipTone of CHIP_TONES) {\n colors[`${hue}${chipTone}` as ChipColorKey] = tone(stops, chipTone);\n }\n // The bare hue is the lightest tone, which is what every existing\n // `color: \"blue\"` in a collection config already means.\n colors[hue] = tone(stops, \"Lighter\");\n }\n return colors;\n}\n\n/**\n * Every chip scheme, keyed by hue and tone.\n *\n * This table used to hold four tones per hue and was flattened to one, which\n * left `blueDark`, `redDarker` and friends resolving to `undefined` — a chip\n * with a colour in its config rendering with no colour at all — and left seeded\n * chips picking from ten schemes, so a five-value enum routinely drew the same\n * background three times. The tones are generated from {@link HUE_STOPS} now,\n * so a new hue brings its whole family with it.\n */\nexport const CHIP_COLORS: Record<ChipColorKey, ChipColorScheme> = buildChipColors();\n\n/**\n * The keys a seeded chip may be assigned, in a stable order.\n *\n * The bare-hue aliases are excluded: they are the same scheme as `<hue>Lighter`\n * and would make the palest tone twice as likely as any other.\n */\nexport const CHIP_SEED_KEYS: ChipColorKey[] = CHIP_HUES.flatMap(\n hue => CHIP_TONES.map(chipTone => `${hue}${chipTone}` as ChipColorKey));\n\n// `string & {}` keeps the known keys in autocomplete while still accepting the\n// arbitrary column colours a board hands over.\nexport function getColorSchemeForKey(key: ChipColorKey | (string & {})): ChipColorScheme {\n // An unknown key is a config that names a colour this build does not have —\n // a renamed hue, or a value typed by hand. Seeding from the key keeps it\n // coloured and keeps it stable, rather than dropping it to no colour.\n return CHIP_COLORS[key as ChipColorKey] ?? getColorSchemeForSeed(String(key));\n}\n\nexport function getColorSchemeForSeed(seed: string): ChipColorScheme {\n const hash: number = hashString(seed);\n return CHIP_COLORS[CHIP_SEED_KEYS[hash % CHIP_SEED_KEYS.length]];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"chip_colors.es.js","names":[],"sources":["../../src/util/chip_colors.ts"],"sourcesContent":["import { hashString } from \"./hash\";\n\nexport type ChipColorScheme = {\n color: string;\n text: string;\n /** Background color override for dark mode */\n darkColor?: string;\n /** Text color override for dark mode */\n darkText?: string;\n /**\n * Ink for the `outlined` variant, which has no fill of its own.\n *\n * `text`/`darkText` are the ink ON the chip's own background. An outlined\n * chip drops that background and sits on the PAGE, so the same value is\n * being asked to be legible against two different surfaces at once — and\n * for most hues it cannot be. Splitting the roles is what lets the filled\n * ink follow its fill (often dark ink on a bright chip) without dragging\n * the outlined variant down with it.\n */\n outlineText?: string;\n /** Outlined-variant ink in dark mode. */\n darkOutlineText?: string;\n /**\n * The `tinted` variant: the hue's solid stop at low alpha, as an `rgba()`\n * string so it composes over a card, the sheet or a dialog alike, and an\n * ink MEASURED on what that tint reads as over the page. A tint is the\n * reference's rule for hue in the chrome — a 12–16% wash behind hue-coloured\n * text — and it is what a status or enum chip paints by default now; the\n * saturated `color` stop is the `filled` variant, kept for swatches.\n */\n tintColor?: string;\n tintText?: string;\n darkTintColor?: string;\n darkTintText?: string;\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Contrast\n ────────────────────────────────────────────────────────────────────────────\n\n Chip ink is DERIVED, not hand-picked. It used to be written down per tone —\n `\"#fff\"` on every `solid` background, the hue's `pale` on every `deep` one —\n and an audit of all 120 hue/tone/mode pairs found **63 of them below WCAG AA**.\n The worst was white on `teal.solid` at **1.76:1**, which is not a near miss;\n it is unreadable. The palette is an Airtable-style one whose mid stops are\n bright enough to need dark ink, and hardcoding light ink ignored that.\n\n So the ink is measured against the background it will actually sit on, and\n pushed toward black or white only as far as it must go to clear the floor.\n Starting from the hue's own dark/light tints rather than from flat `#000`\n and `#fff` keeps the family looking like a family: `blue.solid` gets a very\n dark navy, not black.\n\n Deriving it also means a new hue cannot be added below AA — there is no\n per-tone ink to forget to check. */\n\n/** WCAG floor, plus a little margin so rounding cannot drop a pair below 4.5. */\nconst CONTRAST_TARGET = 4.6;\n\n/** Page backgrounds the outlined variant sits on: `bg-white` and `surface-950`. */\nconst PAGE_LIGHT = \"#ffffff\";\nconst PAGE_DARK = \"#16181c\";\n\nfunction toRgb(hex: string): [number, number, number] {\n let h = hex.replace(\"#\", \"\");\n if (h.length === 3) h = h.split(\"\").map(c => c + c).join(\"\");\n return [0, 2, 4].map(i => parseInt(h.slice(i, i + 2), 16)) as [number, number, number];\n}\n\nfunction toHex(rgb: number[]): string {\n return \"#\" + rgb.map(v => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nfunction toRgba(hex: string, alpha: number): string {\n const [r, g, b] = toRgb(hex);\n return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\n/**\n * What a translucent colour reads as over an opaque one: a per-channel sRGB\n * mix, which is how the browser composites it. The tint ink is pushed against\n * this, never against the page alone — a 16% wash of the hue moves the\n * background a long way toward the ink, and an ink that only cleared AA on\n * the bare page lands near 3.5:1 on its own tint.\n */\nexport function compositeOver(fgHex: string, alpha: number, bgHex: string): string {\n const fg = toRgb(fgHex);\n const bg = toRgb(bgHex);\n return toHex(fg.map((c, i) => alpha * c + (1 - alpha) * bg[i]));\n}\n\n/** Read an `rgba(r, g, b, a)` string back as what it composites to over `bgHex`. */\nexport function tintOnPage(rgba: string, bgHex: string): string {\n const m = rgba.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)(?:,\\s*([\\d.]+))?\\)/);\n if (!m) return rgba;\n const fg = toHex([Number(m[1]), Number(m[2]), Number(m[3])]);\n return compositeOver(fg, m[4] === undefined ? 1 : Number(m[4]), bgHex);\n}\n\n/**\n * Tint alpha by tone, light then dark. The tones keep meaning something in the\n * tinted variant — a `Darker` chip is a heavier wash, not a different hue —\n * and dark mode runs four points higher because the same alpha of a bright\n * hue reads fainter over near-black than over white.\n */\nconst TINT_ALPHA: Record<ChipTone, [light: number, dark: number]> = {\n Lighter: [0.10, 0.14],\n Light: [0.14, 0.18],\n Dark: [0.18, 0.22],\n Darker: [0.22, 0.26]\n};\n\nfunction relativeLuminance(hex: string): number {\n const [r, g, b] = toRgb(hex).map(v => {\n const c = v / 255;\n return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);\n });\n return 0.2126 * r + 0.7152 * g + 0.0722 * b;\n}\n\nexport function contrastRatio(a: string, b: string): number {\n const x = relativeLuminance(a);\n const y = relativeLuminance(b);\n const [hi, lo] = x > y ? [x, y] : [y, x];\n return (hi + 0.05) / (lo + 0.05);\n}\n\nfunction mixHex(from: string, to: string, t: number): string {\n const A = toRgb(from);\n const B = toRgb(to);\n return toHex([0, 1, 2].map(i => A[i] + (B[i] - A[i]) * t));\n}\n\n/**\n * Walk a tinted ink toward black or white until it clears the floor on `bg`.\n *\n * Stops at the first passing step rather than going all the way, so the ink\n * keeps as much of its hue as legibility allows.\n */\nfunction push(base: string, toward: string, bg: string): string {\n for (let t = 0; t <= 1.0001; t += 0.02) {\n const candidate = mixHex(base, toward, t);\n if (contrastRatio(candidate, bg) >= CONTRAST_TARGET) return candidate;\n }\n return toward;\n}\n\n/** The ink for a chip filled with `bg`: whichever direction has more headroom. */\nfunction inkOn(stops: HueStops, bg: string): string {\n const dark = push(stops.text, \"#000000\", bg);\n const light = push(stops.onDeep ?? stops.pale, \"#ffffff\", bg);\n return contrastRatio(dark, bg) >= contrastRatio(light, bg) ? dark : light;\n}\n\n/**\n * The hues a chip can take, and the four stops each one is built from.\n *\n * `pale` and `mid` are backgrounds for dark text; `solid` and `deep` are\n * backgrounds for light text. `text` is the dark ink used on the two pale\n * stops, and `onDeep` the light ink on `deep` — it defaults to `pale`, and is\n * only set where a hue needs a tint of its own to stay legible.\n */\ntype HueStops = {\n pale: string;\n mid: string;\n solid: string;\n deep: string;\n text: string;\n onDeep?: string;\n}\n\nexport const CHIP_HUES = [\n \"blue\", \"cyan\", \"teal\", \"green\", \"yellow\", \"orange\", \"red\", \"pink\", \"purple\", \"gray\",\n \"indigo\", \"violet\", \"fuchsia\", \"rose\", \"emerald\"\n] as const;\n\nexport type ChipHue = typeof CHIP_HUES[number];\n\n/**\n * How light or saturated a chip of a given hue is.\n *\n * A hue on its own (`\"blue\"`) means `Lighter` — the tone the picker offers and\n * the one seeded chips used to be limited to.\n */\nexport type ChipTone = \"Lighter\" | \"Light\" | \"Dark\" | \"Darker\";\n\nexport type ChipColorKey = ChipHue | `${ChipHue}${ChipTone}`;\n\nconst HUE_STOPS: Record<ChipHue, HueStops> = {\n blue: { pale: \"#cfdfff\", mid: \"#9cc7ff\", solid: \"#2d7ff9\", deep: \"#2750ae\", text: \"#102046\" },\n cyan: { pale: \"#d0f0fd\", mid: \"#77d1f3\", solid: \"#18bfff\", deep: \"#0b76b7\", text: \"#04283f\" },\n teal: { pale: \"#c2f5e9\", mid: \"#72ddc3\", solid: \"#20d9d2\", deep: \"#06a09b\", text: \"#012524\", onDeep: \"#daf3e9\" },\n green: { pale: \"#d1f7c4\", mid: \"#93e088\", solid: \"#20c933\", deep: \"#338a17\", text: \"#0b1d05\" },\n yellow: { pale: \"#ffeab6\", mid: \"#ffd66e\", solid: \"#fcb400\", deep: \"#b87503\", text: \"#3b2501\" },\n orange: { pale: \"#fee2d5\", mid: \"#ffa981\", solid: \"#ff6f2c\", deep: \"#d74d26\", text: \"#6b2613\" },\n red: { pale: \"#ffdce5\", mid: \"#ff9eb7\", solid: \"#f82b60\", deep: \"#ba1e45\", text: \"#4c0c1c\" },\n pink: { pale: \"#ffdaf6\", mid: \"#f99de2\", solid: \"#ff08c2\", deep: \"#b2158b\", text: \"#400832\" },\n purple: { pale: \"#ede2fe\", mid: \"#cdb0ff\", solid: \"#8b46ff\", deep: \"#6b1cb0\", text: \"#280b42\" },\n gray: { pale: \"#eceef1\", mid: \"#c9ccd2\", solid: \"#62676e\", deep: \"#40444a\", text: \"#040405\" },\n indigo: { pale: \"#e0e7ff\", mid: \"#a5b4fc\", solid: \"#6366f1\", deep: \"#4f46e5\", text: \"#312e81\" },\n violet: { pale: \"#ede9fe\", mid: \"#c4b5fd\", solid: \"#8b5cf6\", deep: \"#7c3aed\", text: \"#4c1d95\" },\n fuchsia: { pale: \"#fae8ff\", mid: \"#f0abfc\", solid: \"#d946ef\", deep: \"#c026d3\", text: \"#701a75\" },\n rose: { pale: \"#ffe4e6\", mid: \"#fda4af\", solid: \"#f43f5e\", deep: \"#e11d48\", text: \"#881337\" },\n emerald: { pale: \"#d1fae5\", mid: \"#6ee7b7\", solid: \"#10b981\", deep: \"#059669\", text: \"#064e3b\" }\n};\n\n/**\n * One tone of one hue.\n *\n * Backgrounds are the palette's, unchanged — the stops were not the problem and\n * moving them would have restyled every chip in the product. Only the ink moved,\n * and it moved to wherever the measurement says it has to be.\n */\nfunction tone(stops: HueStops, chipTone: ChipTone): ChipColorScheme {\n // The outlined variant is a property of the HUE, not of the tone: it has no\n // fill, so every tone of a hue sits on the same page background and wants\n // the same ink.\n const outline = {\n outlineText: push(stops.text, \"#000000\", PAGE_LIGHT),\n darkOutlineText: push(stops.onDeep ?? stops.pale, \"#ffffff\", PAGE_DARK)\n };\n\n // The tint is the SOLID stop washed out, on both themes, so a hue keeps\n // its identity across the two — a pale-stop tint of blue reads as grey.\n // Light ink starts from `deep` and dark ink from `mid`: the saturated end\n // that already sits on the right side of the composite, pushed only as\n // far as the measurement says.\n const [alphaLight, alphaDark] = TINT_ALPHA[chipTone];\n const tint = {\n tintColor: toRgba(stops.solid, alphaLight),\n tintText: push(stops.deep, \"#000000\", compositeOver(stops.solid, alphaLight, PAGE_LIGHT)),\n darkTintColor: toRgba(stops.solid, alphaDark),\n darkTintText: push(stops.mid, \"#ffffff\", compositeOver(stops.solid, alphaDark, PAGE_DARK))\n };\n\n const filled = (light: string, dark: string): ChipColorScheme => ({\n color: light,\n text: inkOn(stops, light),\n darkColor: dark,\n darkText: inkOn(stops, dark),\n ...outline,\n ...tint\n });\n\n switch (chipTone) {\n case \"Light\":\n return filled(stops.mid, stops.solid);\n case \"Dark\":\n return filled(stops.solid, stops.solid);\n case \"Darker\":\n return filled(stops.deep, stops.deep);\n case \"Lighter\":\n default:\n return filled(stops.pale, stops.deep);\n }\n}\n\nconst CHIP_TONES: ChipTone[] = [\"Lighter\", \"Light\", \"Dark\", \"Darker\"];\n\nfunction buildChipColors(): Record<ChipColorKey, ChipColorScheme> {\n const colors = {} as Record<ChipColorKey, ChipColorScheme>;\n for (const hue of CHIP_HUES) {\n const stops = HUE_STOPS[hue];\n for (const chipTone of CHIP_TONES) {\n colors[`${hue}${chipTone}` as ChipColorKey] = tone(stops, chipTone);\n }\n // The bare hue is the lightest tone, which is what every existing\n // `color: \"blue\"` in a collection config already means.\n colors[hue] = tone(stops, \"Lighter\");\n }\n return colors;\n}\n\n/**\n * Every chip scheme, keyed by hue and tone.\n *\n * This table used to hold four tones per hue and was flattened to one, which\n * left `blueDark`, `redDarker` and friends resolving to `undefined` — a chip\n * with a colour in its config rendering with no colour at all — and left seeded\n * chips picking from ten schemes, so a five-value enum routinely drew the same\n * background three times. The tones are generated from {@link HUE_STOPS} now,\n * so a new hue brings its whole family with it.\n */\nexport const CHIP_COLORS: Record<ChipColorKey, ChipColorScheme> = buildChipColors();\n\n/**\n * The keys a seeded chip may be assigned, in a stable order.\n *\n * The bare-hue aliases are excluded: they are the same scheme as `<hue>Lighter`\n * and would make the palest tone twice as likely as any other.\n */\nexport const CHIP_SEED_KEYS: ChipColorKey[] = CHIP_HUES.flatMap(\n hue => CHIP_TONES.map(chipTone => `${hue}${chipTone}` as ChipColorKey));\n\n// `string & {}` keeps the known keys in autocomplete while still accepting the\n// arbitrary column colours a board hands over.\nexport function getColorSchemeForKey(key: ChipColorKey | (string & {})): ChipColorScheme {\n // An unknown key is a config that names a colour this build does not have —\n // a renamed hue, or a value typed by hand. Seeding from the key keeps it\n // coloured and keeps it stable, rather than dropping it to no colour.\n return CHIP_COLORS[key as ChipColorKey] ?? getColorSchemeForSeed(String(key));\n}\n\nexport function getColorSchemeForSeed(seed: string): ChipColorScheme {\n const hash: number = hashString(seed);\n return CHIP_COLORS[CHIP_SEED_KEYS[hash % CHIP_SEED_KEYS.length]];\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/ui",
3
- "version": "0.25.0-canary.ge06257e",
3
+ "version": "0.25.1",
4
4
  "description": "Rebase design system — React components and design tokens built on Radix UI, Tailwind CSS and lucide-react",
5
5
  "keywords": [
6
6
  "rebase",