Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ yarn parse-schema # Parse JSON schemas for frontend (connection and
- Shadows: `--shadow-xs` through `--shadow-3xl`
- Border radius: `--radius-none` through `--radius-full`
- **Color Usage**: Full token reference, dark mode guide, and anti-pattern cheat sheet: [`openmetadata-ui/src/main/resources/ui/docs/colors.md`](openmetadata-ui/src/main/resources/ui/docs/colors.md). Always consult this before choosing any color class.
- **Borders — never use `tw:ring-*` to draw an edge.** Rings compile to `box-shadow`, which WebKit does not pixel-snap, so they thin out and can vanish in Safari at non-100% zoom. Use `tw:border-*` where the edge may take layout space, or `tw:outline-1 tw:-outline-offset-1 tw:outline-<token>` where it must not. On focusable elements the `outline` is already the focus ring — draw the border on `::after` via `borderAfter` from `@openmetadata/ui-core-components`. Rules, ring→outline translation table, and gotchas (`outline-hidden` erases outline borders; `transition-shadow` won't animate them): [`colors.md` §2.3.1](openmetadata-ui/src/main/resources/ui/docs/colors.md).
- **Legacy**: Ant Design components remain in existing code but should be replaced with `openmetadata-ui-core-components` equivalents when refactoring
- Do not add unnecessary spacing between logs and code.
- In Java, avoid wildcards imports (e.g., use `import java.util.List;` instead of `import java.util.*;`)
Expand Down
46 changes: 46 additions & 0 deletions openmetadata-ui-core-components/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,52 @@ Theme colors are defined as CSS custom properties in `src/styles/globals.css` wi
- Use `cx()` (from `tailwind-merge`) to merge class names with conflict resolution
- Use `isReactComponent()` to type-guard icon props that accept both `FC` and `ReactNode`

### Borders: never use `ring-*` — use `border` or `outline`

`tw:ring-*` compiles to a `box-shadow`, and **WebKit does not pixel-snap box-shadows** — a
ring used as a border thins out and can vanish entirely in Safari at non-100% zoom. `border`
and `outline` are snapped and never degrade. The library was migrated off rings; do not
reintroduce them.

| Situation | Use |
|---|---|
| Edge may occupy layout space (static container) | `tw:border tw:border-<token>` |
| Edge must be layout-neutral and the element's `outline` is free | `tw:outline-1 tw:-outline-offset-1 tw:outline-<token>` |
| Element's `outline` is already the focus ring (any focusable control) | `borderAfter` from `@/utils/tailwindClasses` |

`border` consumes layout, so on content-sized controls it adds 2px of height and makes them
grow on focus (1px → 2px). That is why controls use `outline`.

```tsx
import { borderAfter } from '@/utils/tailwindClasses';

// host needs `tw:relative`; colour via `tw:after:outline-<token>`
cx('tw:relative', borderAfter, 'tw:after:outline-primary',
isDisabled && 'tw:disabled:after:outline-disabled_subtle')
```

`borderAfter2` is the 2px variant. Note `::before` is already used by `button.tsx` and
`social-button.tsx` for their inner gradient — hence `::after`.

Converting a ring: `ring-inset` → `-outline-offset-N`; **no** `ring-inset` → offset `0` (a
non-inset ring draws *outward*). Getting this wrong shifts the edge by 1px.

Gotchas:
- **`tw:outline-hidden` erases an outline border** — remove it from any element whose border
is an outline. Unlayered LESS (`outline: none`) beats Tailwind utilities and will kill it.
- **`tw:transition-shadow` won't animate an outline** — use
`tw:transition-[outline-color,outline-width]`. Plain `tw:transition` covers `outline-color`
but not `outline-width`.
- **Keep `tw:shadow-*`** — a real drop shadow, not the ring.
- Consumers overriding a border must match where it's drawn: `::after` for
Button/ButtonUtility/Tab, the element for Input/Select/Badge/Card.

Rings legitimately remain only where `ring-offset-*` fills the gap with a colour
(`color-picker-field`, `icon-picker-field`) — `outline-offset` leaves it transparent.

Full rationale, measurements, and the anti-pattern table:
[`openmetadata-ui/src/main/resources/ui/docs/colors.md`](../openmetadata-ui/src/main/resources/ui/docs/colors.md) §2.3.1.

### Button Color Variants

The `Button` component supports these `color` values:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const Accordion = ({
<AriaDisclosureGroup
{...props}
className={cx(
'tw:w-full tw:divide-y tw:divide-border-secondary tw:rounded-xl tw:ring-1 tw:ring-border-secondary tw:overflow-hidden',
'tw:w-full tw:divide-y tw:divide-border-secondary tw:rounded-xl tw:outline-1 tw:outline-border-secondary tw:overflow-hidden',
className
)}>
{children}
Expand Down Expand Up @@ -143,10 +143,13 @@ export const AccordionHeader = ({
{...props}
className={(state) =>
cx(
'tw:flex tw:w-full tw:cursor-pointer tw:items-center tw:justify-between tw:gap-3 tw:px-6 tw:py-4 tw:text-left tw:outline-hidden tw:transition tw:duration-200 tw:ease-in-out',
// `outline-hidden` removed: the outline now draws the focus indicator (it
// replaced a ring, which WebKit does not pixel-snap).
'tw:flex tw:w-full tw:cursor-pointer tw:items-center tw:justify-between tw:gap-3 tw:px-6 tw:py-4 tw:text-left tw:transition tw:duration-200 tw:ease-in-out',
'tw:text-sm tw:font-semibold tw:text-primary',
'hover:tw:bg-primary_hover',
state.isFocusVisible && 'tw:ring-2 tw:ring-inset tw:ring-brand-300',
state.isFocusVisible &&
'tw:outline-2 tw:-outline-offset-2 tw:outline-brand-300',
state.isDisabled && 'tw:cursor-not-allowed tw:text-disabled',
typeof className === 'function' ? className(state) : className
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const NavAccountCardMenuItem = ({
</div>

{shortcut && (
<kbd className="tw:flex tw:rounded tw:px-1 tw:py-px tw:font-body tw:text-xs tw:font-medium tw:text-tertiary tw:ring-1 tw:ring-secondary tw:ring-inset">
<kbd className="tw:flex tw:rounded tw:px-1 tw:py-px tw:font-body tw:text-xs tw:font-medium tw:text-tertiary tw:outline-1 tw:-outline-offset-1 tw:outline-secondary">
{shortcut}
</kbd>
)}
Expand Down Expand Up @@ -137,11 +137,15 @@ export const NavAccountMenu = ({
<AriaDialog
{...dialogProps}
className={cx(
'tw:w-66 tw:rounded-xl tw:bg-secondary_alt tw:shadow-lg tw:ring tw:ring-secondary_alt tw:outline-hidden',
// Ported faithfully: the bare `tw:ring` was 3px (Tailwind v4's default) where every
// sibling uses ring-1 — almost certainly unintended, but narrowing it here would be
// a visual change. Tracked as a separate follow-up.
// `outline-hidden` removed: the outline now draws this border.
'tw:w-66 tw:rounded-xl tw:bg-secondary_alt tw:shadow-lg tw:outline-[3px] tw:outline-secondary_alt',
className
)}
ref={dialogRef}>
<div className="tw:rounded-xl tw:bg-primary tw:ring-1 tw:ring-secondary">
<div className="tw:rounded-xl tw:bg-primary tw:outline-1 tw:outline-secondary">
<div className="tw:flex tw:flex-col tw:gap-0.5 tw:py-1.5">
<NavAccountCardMenuItem
icon={User01}
Expand Down Expand Up @@ -230,7 +234,7 @@ export const NavAccountCard = ({

return (
<div
className="tw:relative tw:flex tw:items-center tw:gap-3 tw:rounded-xl tw:p-3 tw:ring-1 tw:ring-secondary tw:ring-inset"
className="tw:relative tw:flex tw:items-center tw:gap-3 tw:rounded-xl tw:p-3 tw:outline-1 tw:-outline-offset-1 tw:outline-secondary"
ref={triggerRef}>
<AvatarLabelGroup
size="md"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ const styles = sortCx({
'button-white': {
link: 'tw:rounded-md tw:text-quaternary tw:hover:bg-primary_hover tw:hover:text-secondary',
current:
'tw:rounded-md tw:bg-primary tw:font-medium tw:text-secondary tw:shadow-xs tw:ring-1 tw:ring-primary tw:ring-inset',
'tw:rounded-md tw:bg-primary tw:font-medium tw:text-secondary tw:shadow-xs tw:outline-1 tw:-outline-offset-1 tw:outline-primary',
},
'button-gray': {
link: 'tw:rounded-md tw:text-quaternary tw:hover:bg-secondary tw:hover:text-secondary',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export const DateInput = (props: DateInputProps) => {
<AriaDateInput
{...props}
className={cx(
'tw:flex tw:rounded-lg tw:bg-primary tw:px-2.5 tw:py-2 tw:text-md tw:shadow-xs tw:ring-1 tw:ring-primary tw:ring-inset tw:focus-within:ring-2 tw:focus-within:ring-brand',
// Border drawn with outline, not a ring: WebKit does not pixel-snap box-shadow,
// so rings thin/vanish in Safari when zoomed out.
'tw:flex tw:rounded-lg tw:bg-primary tw:px-2.5 tw:py-2 tw:text-md tw:shadow-xs tw:outline-1 tw:-outline-offset-1 tw:outline-primary tw:focus-within:outline-2 tw:focus-within:-outline-offset-2 tw:focus-within:outline-brand',
typeof props.className === 'string' && props.className
)}>
{(segment) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ export const DatePicker = ({
}
offset={8}
placement="bottom right">
<AriaDialog className="tw:rounded-2xl tw:bg-primary tw:shadow-xl tw:ring tw:ring-secondary_alt">
{/* outline-[3px] ports the bare `tw:ring` faithfully (3px in Tailwind v4, almost
certainly unintended vs the ring-1 used elsewhere — tracked as a follow-up). */}
<AriaDialog className="tw:rounded-2xl tw:bg-primary tw:shadow-xl tw:outline-[3px] tw:outline-secondary_alt">
{({ close }) => (
<>
<div className="tw:flex tw:px-6 tw:py-5">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,10 @@ export const DateRangePicker = ({
}
offset={8}
placement="bottom right">
<AriaDialog className="tw:flex tw:rounded-2xl tw:bg-primary tw:shadow-xl tw:ring tw:ring-secondary_alt tw:focus:outline-hidden">
{/* `outline-[3px]` ports the bare `tw:ring` faithfully (3px in Tailwind v4 — likely
unintended vs ring-1 elsewhere; tracked as a follow-up). `focus:outline-hidden`
removed: it would suppress this border. */}
<AriaDialog className="tw:flex tw:rounded-2xl tw:bg-primary tw:shadow-xl tw:outline-[3px] tw:outline-secondary_alt">
{({ close }) => (
<>
<div className="tw:hidden tw:w-38 tw:flex-col tw:gap-0.5 tw:border-r tw:border-solid tw:border-secondary tw:p-3 tw:lg:flex">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,10 @@ export const FileUploadDropZone = ({
aria-disabled={isDisabled}
aria-labelledby={`${id}-label`}
className={cx(
'tw:relative tw:flex tw:flex-col tw:items-center tw:gap-3 tw:rounded-xl tw:bg-primary tw:px-6 tw:py-4 tw:text-tertiary tw:ring-1 tw:ring-secondary tw:transition tw:duration-100 tw:ease-linear tw:ring-inset',
isDraggingOver && 'tw:ring-2 tw:ring-brand',
// Border drawn with outline, not a ring: WebKit does not pixel-snap box-shadow, so
// rings thin/vanish in Safari when zoomed out.
'tw:relative tw:flex tw:flex-col tw:items-center tw:gap-3 tw:rounded-xl tw:bg-primary tw:px-6 tw:py-4 tw:text-tertiary tw:outline-1 tw:-outline-offset-1 tw:outline-secondary tw:transition tw:duration-100 tw:ease-linear',
isDraggingOver && 'tw:outline-2 tw:-outline-offset-2 tw:outline-brand',
isDisabled
? 'tw:cursor-not-allowed tw:bg-secondary'
: 'tw:cursor-pointer',
Expand Down Expand Up @@ -342,8 +344,10 @@ export const FileListItemProgressBar = ({
return (
<li
className={cx(
'tw:relative tw:flex tw:gap-3 tw:rounded-xl tw:bg-primary tw:p-4 tw:ring-1 tw:ring-secondary tw:transition-shadow tw:duration-100 tw:ease-linear tw:ring-inset',
failed && 'tw:ring-2 tw:ring-error',
// Border drawn with outline, not a ring. `transition-shadow` animated only
// box-shadow, so it must name the outline properties now.
'tw:relative tw:flex tw:gap-3 tw:rounded-xl tw:bg-primary tw:p-4 tw:outline-1 tw:-outline-offset-1 tw:outline-secondary tw:transition-[outline-color,outline-width] tw:duration-100 tw:ease-linear',
failed && 'tw:outline-2 tw:-outline-offset-2 tw:outline-error',
className
)}>
<FileIcon
Expand Down Expand Up @@ -468,8 +472,8 @@ export const FileListItemProgressFill = ({
/>
<div
className={cx(
'tw:absolute tw:inset-0 tw:size-full tw:rounded-[inherit] tw:ring-1 tw:ring-secondary tw:transition tw:duration-100 tw:ease-linear tw:ring-inset',
failed && 'tw:ring-2 tw:ring-error'
'tw:absolute tw:inset-0 tw:size-full tw:rounded-[inherit] tw:outline-1 tw:-outline-offset-1 tw:outline-secondary tw:transition tw:duration-100 tw:ease-linear',
failed && 'tw:outline-2 tw:-outline-offset-2 tw:outline-error'
)}
/>
<FileIcon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -514,8 +514,8 @@ export const CoverImageUploadField = ({
<div
aria-invalid={isInvalid || undefined}
className={cx(
'tw:group tw:relative tw:w-full tw:overflow-hidden tw:rounded-lg tw:ring-1 tw:ring-secondary',
isInvalid && 'tw:ring-2 tw:ring-error',
'tw:group tw:relative tw:w-full tw:overflow-hidden tw:rounded-lg tw:outline-1 tw:outline-secondary',
isInvalid && 'tw:outline-2 tw:outline-error',
previewClassName
)}
data-testid="cover-image-upload-preview-container"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ export const IconPickerField = ({
/>

{isOpen && (
<div className="tw:absolute tw:top-[calc(100%+8px)] tw:left-0 tw:z-50 tw:w-[22rem] tw:max-w-[calc(100vw-2rem)] tw:rounded-xl tw:bg-primary tw:shadow-lg tw:ring-1 tw:ring-secondary_alt">
<div className="tw:absolute tw:top-[calc(100%+8px)] tw:left-0 tw:z-50 tw:w-[22rem] tw:max-w-[calc(100vw-2rem)] tw:rounded-xl tw:bg-primary tw:shadow-lg tw:outline-1 tw:outline-secondary_alt">
{allowUrl ? (
<Tabs
selectedKey={activeTab}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,12 @@ const compactPageInputClassName = cx(
'tw:focus-visible:outline-none'
);

// The Select trigger's border is an outline now (it replaced a ring, which WebKit does not
// pixel-snap). The old `outline-none!` only suppressed the native focus outline and was
// harmless; keeping it would erase the border itself. `focus-visible:outline-none!` stays —
// it reproduces what `focus-visible:ring-0` used to do (no edge while focused).
const compactRowsPerPageSelectClassName =
'tw:w-[42px] tw:shrink-0 tw:gap-0 tw:[&>button]:h-6 tw:[&>button]:rounded-lg tw:[&>button]:outline-none! tw:[&>button]:focus-visible:outline-none! tw:[&>button]:focus-visible:ring-0 tw:[&>button]:focus-visible:ring-offset-0 tw:[&>button>span]:h-6 tw:[&>button>span]:gap-1 tw:[&>button>span]:px-1 tw:[&>button>span]:py-0 tw:[&>button>span>section]:min-w-0 tw:[&>button>span>section]:gap-0 tw:[&>button>span>section>p]:min-w-4 tw:[&>button>span>section>p]:text-center tw:[&>button>span>section>p]:text-xs tw:[&>button>span>section>p]:leading-[18px] tw:[&>button>span>svg]:size-3';
'tw:w-[42px] tw:shrink-0 tw:gap-0 tw:[&>button]:h-6 tw:[&>button]:rounded-lg tw:[&>button]:focus-visible:outline-none! tw:[&>button>span]:h-6 tw:[&>button>span]:gap-1 tw:[&>button>span]:px-1 tw:[&>button>span]:py-0 tw:[&>button>span>section]:min-w-0 tw:[&>button>span>section]:gap-0 tw:[&>button>span>section>p]:min-w-4 tw:[&>button>span>section>p]:text-center tw:[&>button>span>section>p]:text-xs tw:[&>button>span>section>p]:leading-[18px] tw:[&>button>span>svg]:size-3';

const compactRowsPerPageItemClassName =
'tw:px-0 tw:[&>div]:h-8 tw:[&>div]:px-2 tw:[&>div]:py-0 tw:[&>div]:outline-none! tw:[&[data-focused]>div]:bg-primary_hover tw:[&[data-selected]>div]:bg-primary tw:[&>div>div]:min-w-0 tw:[&>div>div>span]:text-xs tw:[&>div>div>span]:leading-[18px]';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ export const Popover = ({
{...popoverProps}
className={(state) =>
cx(
'tw:origin-(--trigger-anchor-point) tw:rounded-xl tw:bg-primary tw:shadow-lg tw:ring-1 tw:ring-secondary_alt tw:outline-hidden tw:will-change-transform',
// `outline-hidden` removed: the outline now draws this popover's border (it
// replaced a ring, which WebKit does not pixel-snap), so suppressing it would
// erase the border.
'tw:origin-(--trigger-anchor-point) tw:rounded-xl tw:bg-primary tw:shadow-lg tw:outline-1 tw:outline-secondary_alt tw:will-change-transform',
state.isEntering &&
'tw:duration-150 tw:ease-out tw:animate-in tw:fade-in tw:placement-left:slide-in-from-right-0.5 tw:placement-right:slide-in-from-left-0.5 tw:placement-top:slide-in-from-bottom-0.5 tw:placement-bottom:slide-in-from-top-0.5',
state.isExiting &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@ const sizes = sortCx({
const circleStatusStyles = sortCx({
complete: 'tw:bg-brand-solid tw:text-fg-white',
current:
'tw:bg-brand-secondary tw:text-fg-brand-primary tw:ring-2 tw:ring-inset tw:ring-bg-brand-solid',
'tw:bg-brand-secondary tw:text-fg-brand-primary tw:outline-2 tw:-outline-offset-2 tw:outline-bg-brand-solid',
incomplete:
'tw:bg-primary tw:text-fg-quaternary tw:ring-1 tw:ring-inset tw:ring-primary',
'tw:bg-primary tw:text-fg-quaternary tw:outline-1 tw:-outline-offset-1 tw:outline-primary',
});

const titleStatusStyles = sortCx({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ export const Dialog = (props: DialogProps) => (
role="dialog"
{...props}
className={cx(
'tw:relative tw:flex tw:size-full tw:flex-col tw:items-start tw:gap-6 tw:overflow-y-auto tw:bg-primary tw:ring-1 tw:ring-secondary_alt tw:outline-hidden',
// `outline-hidden` removed: the outline now draws this panel's border (it replaced a
// ring, which WebKit does not pixel-snap), so suppressing it would erase the border.
'tw:relative tw:flex tw:size-full tw:flex-col tw:items-start tw:gap-6 tw:overflow-y-auto tw:bg-primary tw:outline-1 tw:outline-secondary_alt',
props.className
)}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const TableCardRoot = ({
<div
{...props}
className={cx(
'tw:overflow-hidden tw:rounded-xl tw:bg-primary tw:shadow-xs tw:ring-1 tw:ring-secondary',
'tw:overflow-hidden tw:rounded-xl tw:bg-primary tw:shadow-xs tw:outline-1 tw:outline-secondary',
className
)}>
{children}
Expand Down Expand Up @@ -259,7 +259,11 @@ const TableHead = ({
{...props}
className={(state) =>
cx(
'tw:relative tw:p-0 tw:px-6 tw:py-2 tw:outline-hidden tw:focus-visible:z-1 tw:focus-visible:ring-2 tw:focus-visible:ring-focus-ring tw:focus-visible:ring-offset-bg-primary tw:focus-visible:ring-inset',
// Focus indicator drawn with outline, not a ring (WebKit does not pixel-snap
// box-shadow). `outline-hidden` is gone — it would suppress this indicator.
// `ring-offset-bg-primary` is dropped: it set an offset *colour* while
// --tw-ring-offset-width defaults to 0px, so it never rendered.
'tw:relative tw:p-0 tw:px-6 tw:py-2 tw:focus-visible:z-1 tw:focus-visible:outline-2 tw:focus-visible:-outline-offset-2 tw:focus-visible:outline-focus-ring',
selectionBehavior === 'toggle' && 'tw:nth-2:pl-3',
state.allowsSorting && 'tw:cursor-pointer',
typeof className === 'function' ? className(state) : className
Expand Down
Loading
Loading