@cosxai/ui 0.25.0 → 1.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +577 -0
- package/NOTICE +11 -0
- package/README.md +52 -0
- package/package.json +33 -7
- package/src/fonts.css +20 -0
- package/src/index.ts +8 -25
- package/src/lib/cn.ts +23 -6
- package/src/styles.css +17 -0
- package/src/theme.css +132 -0
- package/src/tokens/colors.css +112 -0
- package/src/tokens/compat.css +36 -0
- package/src/tokens/elevation.css +14 -0
- package/src/tokens/motion.css +42 -0
- package/src/tokens/spacing.css +55 -0
- package/src/tokens/typography-cjk.css +42 -0
- package/src/tokens/typography.css +81 -0
- package/src/actionbar/ActionBar.tsx +0 -837
- package/src/actionbar/ActionBarButton.tsx +0 -111
- package/src/actionbar/ActionBarMenuGroup.tsx +0 -116
- package/src/actionbar/ActionBarModeHandle.tsx +0 -171
- package/src/actionbar/ActionBarProvider.tsx +0 -91
- package/src/actionbar/actionbar-context.ts +0 -45
- package/src/actionbar/index.ts +0 -28
- package/src/actionbar/types.ts +0 -147
- package/src/actionbar/useActionBarItems.ts +0 -60
- package/src/actionbar/useActionBarMode.ts +0 -144
- package/src/actionbar/useActionBarPanel.ts +0 -56
- package/src/actionbar/useActionBarStatusDot.ts +0 -49
- package/src/actionbar/useActionBarToast.ts +0 -33
- package/src/ambient/AmbientBackdrop.tsx +0 -74
- package/src/ambient/CommandInput.tsx +0 -107
- package/src/ambient/SuperbarStrip.tsx +0 -36
- package/src/ambient/index.ts +0 -6
- package/src/bento/BentoCell.tsx +0 -66
- package/src/bento/BentoGrid.tsx +0 -42
- package/src/bento/index.ts +0 -2
- package/src/command/CommandPalette.tsx +0 -329
- package/src/command/CommandProvider.tsx +0 -57
- package/src/command/command-context.ts +0 -12
- package/src/command/index.ts +0 -6
- package/src/command/rank.ts +0 -50
- package/src/command/types.ts +0 -37
- package/src/command/useCommandSource.ts +0 -37
- package/src/dialogs/DialogsProvider.tsx +0 -216
- package/src/dialogs/Modal.tsx +0 -260
- package/src/dialogs/Toast.tsx +0 -85
- package/src/dialogs/dialogs-context.ts +0 -6
- package/src/dialogs/index.ts +0 -10
- package/src/dialogs/types.ts +0 -37
- package/src/dialogs/useDialogs.ts +0 -8
- package/src/editorial/EditorialSpotlight.tsx +0 -63
- package/src/editorial/Folio.tsx +0 -52
- package/src/editorial/PlateMarker.tsx +0 -33
- package/src/editorial/RomanSection.tsx +0 -65
- package/src/editorial/RunningMarginalia.tsx +0 -65
- package/src/editorial/index.ts +0 -10
- package/src/hooks/index.ts +0 -5
- package/src/hooks/useKeyboardHotkey.ts +0 -93
- package/src/hooks/useReducedMotion.ts +0 -20
- package/src/hooks/useViewport.ts +0 -61
- package/src/layout/Breadcrumb.tsx +0 -74
- package/src/layout/LeftNavRail.tsx +0 -139
- package/src/layout/MobileTabBar.tsx +0 -101
- package/src/layout/NavItem.tsx +0 -128
- package/src/layout/NavSearchTrigger.tsx +0 -88
- package/src/layout/NavSection.tsx +0 -40
- package/src/layout/RightSidebarPanel.tsx +0 -111
- package/src/layout/Shell.tsx +0 -91
- package/src/layout/SidePanel.tsx +0 -316
- package/src/layout/StickyBanner.tsx +0 -83
- package/src/layout/Topbar.tsx +0 -68
- package/src/layout/index.ts +0 -24
- package/src/layout/useNavRailState.ts +0 -69
- package/src/lib/time-utils.ts +0 -44
- package/src/neobrutalism/Marquee.tsx +0 -81
- package/src/neobrutalism/Sticker.tsx +0 -71
- package/src/neobrutalism/index.ts +0 -4
- package/src/primitives/Avatar.tsx +0 -53
- package/src/primitives/Button.tsx +0 -67
- package/src/primitives/Card.tsx +0 -41
- package/src/primitives/Checkbox.tsx +0 -87
- package/src/primitives/Chip.tsx +0 -191
- package/src/primitives/Combobox.tsx +0 -507
- package/src/primitives/CopyField.tsx +0 -139
- package/src/primitives/CountBadge.tsx +0 -50
- package/src/primitives/Input.tsx +0 -204
- package/src/primitives/Kbd.tsx +0 -45
- package/src/primitives/MentionCombobox.tsx +0 -612
- package/src/primitives/MultiCombobox.tsx +0 -327
- package/src/primitives/PageHeader.tsx +0 -77
- package/src/primitives/Radio.tsx +0 -131
- package/src/primitives/SegmentedControl.tsx +0 -135
- package/src/primitives/Select.tsx +0 -779
- package/src/primitives/SignaturePad.tsx +0 -461
- package/src/primitives/Tag.tsx +0 -56
- package/src/primitives/Textarea.tsx +0 -62
- package/src/primitives/ToggleSwitch.tsx +0 -79
- package/src/primitives/Tooltip.tsx +0 -213
- package/src/primitives/index.ts +0 -53
- package/src/pwa/InstallPromptBanner.tsx +0 -132
- package/src/pwa/index.ts +0 -4
- package/src/pwa/manifest.template.json +0 -20
- package/src/pwa/registerSW.ts +0 -55
- package/src/riso/Halftone.tsx +0 -85
- package/src/riso/Misregister.tsx +0 -63
- package/src/riso/RisoStamp.tsx +0 -76
- package/src/riso/index.ts +0 -3
- package/src/sketch/HandUnderline.tsx +0 -53
- package/src/sketch/RoughArrow.tsx +0 -91
- package/src/sketch/RoughBox.tsx +0 -73
- package/src/sketch/StickyNote.tsx +0 -56
- package/src/sketch/index.ts +0 -4
- package/src/styles/base.css +0 -186
- package/src/styles/chrome-ambient.css +0 -235
- package/src/styles/chrome-bento.css +0 -192
- package/src/styles/chrome-editorial.css +0 -168
- package/src/styles/chrome-neobrutalism.css +0 -326
- package/src/styles/chrome-riso.css +0 -339
- package/src/styles/chrome-sketch.css +0 -362
- package/src/styles/chrome-swiss.css +0 -265
- package/src/styles/chrome-terminal.css +0 -272
- package/src/styles/fonts.css +0 -1034
- package/src/styles/index.css +0 -343
- package/src/styles/tokens.css +0 -941
- package/src/terminal/AsciiBox.tsx +0 -65
- package/src/terminal/BrailleSpinner.tsx +0 -46
- package/src/terminal/index.ts +0 -4
- package/src/theme/ThemeProvider.tsx +0 -99
- package/src/theme/index.ts +0 -6
- package/src/theme/inline-script.ts +0 -40
- package/src/theme/theme-context.ts +0 -7
- package/src/theme/types.ts +0 -39
- package/src/theme/useTheme.ts +0 -8
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type { ReactNode, CSSProperties } from "react";
|
|
2
|
-
|
|
3
|
-
// Rotated badge — "NEW!", "FREE!", "v2.0!" style. Sits in the
|
|
4
|
-
// corner of featured cards or banners. Filled with an accent
|
|
5
|
-
// colour, thick border, hard shadow, slight tilt to read as a
|
|
6
|
-
// physical sticker pinned on the page.
|
|
7
|
-
|
|
8
|
-
export interface StickerProps {
|
|
9
|
-
children: ReactNode;
|
|
10
|
-
// Tilt angle in degrees. Default -8 (slight left lean —
|
|
11
|
-
// pleasing eye-tilt without going crazy).
|
|
12
|
-
rotate?: number;
|
|
13
|
-
// Fill tone. "accent" / "warning" / "success" / "critical"
|
|
14
|
-
// pick from token palette; pass a hex string to override.
|
|
15
|
-
tone?: "accent" | "warning" | "success" | "critical" | string;
|
|
16
|
-
// px diameter — sticker is square so width == height.
|
|
17
|
-
size?: number;
|
|
18
|
-
className?: string;
|
|
19
|
-
style?: CSSProperties;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function toneToBg(tone: NonNullable<StickerProps["tone"]>) {
|
|
23
|
-
switch (tone) {
|
|
24
|
-
case "accent":
|
|
25
|
-
return "var(--ck-accent, #88AAEE)";
|
|
26
|
-
case "warning":
|
|
27
|
-
return "var(--ck-warning, #FFD23F)";
|
|
28
|
-
case "success":
|
|
29
|
-
return "var(--ck-success, #A3E636)";
|
|
30
|
-
case "critical":
|
|
31
|
-
return "var(--ck-critical, #FF6B6B)";
|
|
32
|
-
default:
|
|
33
|
-
return tone;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function Sticker({
|
|
38
|
-
children,
|
|
39
|
-
rotate = -8,
|
|
40
|
-
tone = "warning",
|
|
41
|
-
size = 80,
|
|
42
|
-
className,
|
|
43
|
-
style,
|
|
44
|
-
}: StickerProps) {
|
|
45
|
-
return (
|
|
46
|
-
<span
|
|
47
|
-
className={className}
|
|
48
|
-
style={{
|
|
49
|
-
display: "inline-flex",
|
|
50
|
-
alignItems: "center",
|
|
51
|
-
justifyContent: "center",
|
|
52
|
-
textAlign: "center",
|
|
53
|
-
width: size,
|
|
54
|
-
height: size,
|
|
55
|
-
padding: 6,
|
|
56
|
-
background: toneToBg(tone),
|
|
57
|
-
color: "#000",
|
|
58
|
-
border: "2px solid var(--neobrut-ink, #000)",
|
|
59
|
-
borderRadius: "50%",
|
|
60
|
-
boxShadow: "3px 3px 0 0 var(--neobrut-ink, #000)",
|
|
61
|
-
transform: `rotate(${rotate}deg)`,
|
|
62
|
-
font: "800 14px/1.1 var(--ck-font-display, var(--ck-font-sans))",
|
|
63
|
-
letterSpacing: "0.02em",
|
|
64
|
-
textTransform: "uppercase",
|
|
65
|
-
...style,
|
|
66
|
-
}}
|
|
67
|
-
>
|
|
68
|
-
{children}
|
|
69
|
-
</span>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { cn } from "../lib/cn";
|
|
2
|
-
|
|
3
|
-
// Circular avatar — image with initials fallback. Use the `name`
|
|
4
|
-
// prop to derive initials (first letter of first two whitespace-
|
|
5
|
-
// separated tokens). Size scales the whole thing including font.
|
|
6
|
-
|
|
7
|
-
export interface AvatarProps {
|
|
8
|
-
// Display name — used for initials fallback and as alt text.
|
|
9
|
-
name: string;
|
|
10
|
-
// Image src. If omitted or fails to load, falls back to initials.
|
|
11
|
-
src?: string | null;
|
|
12
|
-
size?: number; // px, default 32
|
|
13
|
-
className?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function initials(name: string): string {
|
|
17
|
-
const parts = name.trim().split(/\s+/).slice(0, 2);
|
|
18
|
-
return parts.map((p) => p[0]?.toUpperCase() ?? "").join("") || "?";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function Avatar({ name, src, size = 32, className }: AvatarProps) {
|
|
22
|
-
return (
|
|
23
|
-
<span
|
|
24
|
-
className={cn("ck-avatar", className)}
|
|
25
|
-
style={{
|
|
26
|
-
display: "inline-flex",
|
|
27
|
-
alignItems: "center",
|
|
28
|
-
justifyContent: "center",
|
|
29
|
-
width: size,
|
|
30
|
-
height: size,
|
|
31
|
-
borderRadius: "50%",
|
|
32
|
-
background: "var(--ck-bg-muted)",
|
|
33
|
-
color: "var(--ck-text-secondary)",
|
|
34
|
-
font: `500 ${Math.round(size * 0.4)}px/1 var(--ck-font-sans)`,
|
|
35
|
-
overflow: "hidden",
|
|
36
|
-
flexShrink: 0,
|
|
37
|
-
}}
|
|
38
|
-
aria-label={name}
|
|
39
|
-
>
|
|
40
|
-
{src ? (
|
|
41
|
-
<img
|
|
42
|
-
src={src}
|
|
43
|
-
alt={name}
|
|
44
|
-
width={size}
|
|
45
|
-
height={size}
|
|
46
|
-
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
|
47
|
-
/>
|
|
48
|
-
) : (
|
|
49
|
-
initials(name)
|
|
50
|
-
)}
|
|
51
|
-
</span>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
|
|
2
|
-
import { cn } from "../lib/cn";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Button — primitive interactive element. Variants `primary`
|
|
6
|
-
* (filled) / `secondary` (outlined) / `ghost` (transparent) /
|
|
7
|
-
* `icon` (square 32×32).
|
|
8
|
-
*
|
|
9
|
-
* The `loading` prop is the canonical way to show that an async
|
|
10
|
-
* action triggered by the button is in flight. When true:
|
|
11
|
-
*
|
|
12
|
-
* - a small ring spinner renders before the children (inherits
|
|
13
|
-
* `currentColor` so it reads on every variant),
|
|
14
|
-
* - the button is `disabled` natively so clicks are dropped,
|
|
15
|
-
* - `aria-busy="true"` is set for AT consumers.
|
|
16
|
-
*
|
|
17
|
-
* `loading` and `disabled` are distinct states. `disabled` says
|
|
18
|
-
* "you can't take this action right now" (form invalid, no
|
|
19
|
-
* selection, etc.). `loading` says "we're already taking this
|
|
20
|
-
* action — wait". Callers typically wire both:
|
|
21
|
-
*
|
|
22
|
-
* <Button disabled={!name.trim()} loading={submitting}>
|
|
23
|
-
* {submitting ? "Saving…" : "Save"}
|
|
24
|
-
* </Button>
|
|
25
|
-
*
|
|
26
|
-
* Visual: see docs/components/button.
|
|
27
|
-
*
|
|
28
|
-
* Forwards ref to the underlying <button>. Spreads `...rest` so
|
|
29
|
-
* consumers can pass data-* / aria-* / className.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
export type ButtonVariant = "primary" | "secondary" | "ghost" | "icon";
|
|
33
|
-
|
|
34
|
-
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
35
|
-
variant?: ButtonVariant | undefined;
|
|
36
|
-
/**
|
|
37
|
-
* When true, the button renders a leading spinner, sets
|
|
38
|
-
* `aria-busy="true"`, and becomes natively `disabled` so click
|
|
39
|
-
* handlers no longer fire. Use this for any async submit so the
|
|
40
|
-
* user sees the click landed AND can't double-fire the action.
|
|
41
|
-
*/
|
|
42
|
-
loading?: boolean | undefined;
|
|
43
|
-
children?: ReactNode;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
47
|
-
function Button(
|
|
48
|
-
{ variant = "primary", className, children, loading, disabled, ...rest },
|
|
49
|
-
ref,
|
|
50
|
-
) {
|
|
51
|
-
return (
|
|
52
|
-
<button
|
|
53
|
-
ref={ref}
|
|
54
|
-
{...rest}
|
|
55
|
-
className={cn("ck-btn", `ck-btn--${variant}`, className)}
|
|
56
|
-
disabled={disabled || loading}
|
|
57
|
-
aria-busy={loading || undefined}
|
|
58
|
-
data-loading={loading || undefined}
|
|
59
|
-
>
|
|
60
|
-
{loading ? (
|
|
61
|
-
<span className="ck-btn-spinner" aria-hidden="true" />
|
|
62
|
-
) : null}
|
|
63
|
-
{children}
|
|
64
|
-
</button>
|
|
65
|
-
);
|
|
66
|
-
},
|
|
67
|
-
);
|
package/src/primitives/Card.tsx
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { ReactNode, CSSProperties } from "react";
|
|
2
|
-
import { cn } from "../lib/cn";
|
|
3
|
-
|
|
4
|
-
export interface CardProps {
|
|
5
|
-
// Eyebrow + title bar at the top. Pass any nodes; common pattern
|
|
6
|
-
// is <span className="ck-eyebrow">...</span> + <h3>...</h3>.
|
|
7
|
-
header?: ReactNode;
|
|
8
|
-
// Right-aligned actions next to the header (e.g. action buttons).
|
|
9
|
-
headerActions?: ReactNode;
|
|
10
|
-
// Bottom strip (typically buttons or meta info).
|
|
11
|
-
footer?: ReactNode;
|
|
12
|
-
children?: ReactNode;
|
|
13
|
-
// Disable the default body padding (when the children manage
|
|
14
|
-
// their own — e.g. a list with full-bleed rows).
|
|
15
|
-
noPad?: boolean;
|
|
16
|
-
className?: string;
|
|
17
|
-
style?: CSSProperties;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function Card({
|
|
21
|
-
header,
|
|
22
|
-
headerActions,
|
|
23
|
-
footer,
|
|
24
|
-
children,
|
|
25
|
-
noPad,
|
|
26
|
-
className,
|
|
27
|
-
style,
|
|
28
|
-
}: CardProps) {
|
|
29
|
-
return (
|
|
30
|
-
<section className={cn("ck-card", className)} style={style}>
|
|
31
|
-
{(header || headerActions) && (
|
|
32
|
-
<header className="ck-card__head">
|
|
33
|
-
<div style={{ flex: 1, minWidth: 0 }}>{header}</div>
|
|
34
|
-
{headerActions}
|
|
35
|
-
</header>
|
|
36
|
-
)}
|
|
37
|
-
<div className={noPad ? undefined : "ck-card__body"}>{children}</div>
|
|
38
|
-
{footer && <footer className="ck-card__foot">{footer}</footer>}
|
|
39
|
-
</section>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { cn } from "../lib/cn";
|
|
2
|
-
|
|
3
|
-
// Custom-styled checkbox — accent-bordered square that becomes filled
|
|
4
|
-
// accent + white tick when checked. The native <input type="checkbox">
|
|
5
|
-
// renders as a giant white card on dark backgrounds in most browsers,
|
|
6
|
-
// which is exactly the bug that prompted this primitive in the parent
|
|
7
|
-
// project. Use this anywhere you'd reach for a checkbox.
|
|
8
|
-
|
|
9
|
-
export interface CheckboxProps {
|
|
10
|
-
checked: boolean;
|
|
11
|
-
onChange: (next: boolean) => void;
|
|
12
|
-
label?: React.ReactNode;
|
|
13
|
-
suffix?: React.ReactNode;
|
|
14
|
-
disabled?: boolean;
|
|
15
|
-
className?: string;
|
|
16
|
-
/**
|
|
17
|
-
* Test hook forwarded to the visual button. Consumers with
|
|
18
|
-
* getByTestId('…') queries can keep them working across the swap
|
|
19
|
-
* from native <input type="checkbox"> to this primitive without
|
|
20
|
-
* touching each test file.
|
|
21
|
-
*/
|
|
22
|
-
"data-testid"?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function Checkbox({
|
|
26
|
-
checked,
|
|
27
|
-
onChange,
|
|
28
|
-
label,
|
|
29
|
-
suffix,
|
|
30
|
-
disabled,
|
|
31
|
-
className,
|
|
32
|
-
"data-testid": testId,
|
|
33
|
-
}: CheckboxProps) {
|
|
34
|
-
return (
|
|
35
|
-
<label
|
|
36
|
-
className={cn("ck-checkbox", className)}
|
|
37
|
-
style={{
|
|
38
|
-
display: "inline-flex",
|
|
39
|
-
alignItems: "center",
|
|
40
|
-
gap: 8,
|
|
41
|
-
font: "400 13px/1 var(--ck-font-sans)",
|
|
42
|
-
color: "var(--ck-text-secondary)",
|
|
43
|
-
cursor: disabled ? "not-allowed" : "pointer",
|
|
44
|
-
opacity: disabled ? 0.5 : 1,
|
|
45
|
-
userSelect: "none",
|
|
46
|
-
}}
|
|
47
|
-
>
|
|
48
|
-
<button
|
|
49
|
-
type="button"
|
|
50
|
-
role="checkbox"
|
|
51
|
-
aria-checked={checked}
|
|
52
|
-
disabled={disabled}
|
|
53
|
-
onClick={() => !disabled && onChange(!checked)}
|
|
54
|
-
data-testid={testId}
|
|
55
|
-
style={{
|
|
56
|
-
width: 16,
|
|
57
|
-
height: 16,
|
|
58
|
-
borderRadius: 4,
|
|
59
|
-
border: "1.5px solid var(--ck-accent)",
|
|
60
|
-
background: checked ? "var(--ck-accent)" : "transparent",
|
|
61
|
-
display: "inline-flex",
|
|
62
|
-
alignItems: "center",
|
|
63
|
-
justifyContent: "center",
|
|
64
|
-
padding: 0,
|
|
65
|
-
cursor: disabled ? "not-allowed" : "pointer",
|
|
66
|
-
flexShrink: 0,
|
|
67
|
-
}}
|
|
68
|
-
>
|
|
69
|
-
{checked && (
|
|
70
|
-
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" aria-hidden>
|
|
71
|
-
<path
|
|
72
|
-
d="M2.5 6.5L5 9L9.5 3.5"
|
|
73
|
-
stroke="#FFFFFF"
|
|
74
|
-
strokeWidth="2"
|
|
75
|
-
strokeLinecap="round"
|
|
76
|
-
strokeLinejoin="round"
|
|
77
|
-
/>
|
|
78
|
-
</svg>
|
|
79
|
-
)}
|
|
80
|
-
</button>
|
|
81
|
-
{label}
|
|
82
|
-
{suffix && (
|
|
83
|
-
<span style={{ color: "var(--ck-text-tertiary)" }}>{suffix}</span>
|
|
84
|
-
)}
|
|
85
|
-
</label>
|
|
86
|
-
);
|
|
87
|
-
}
|
package/src/primitives/Chip.tsx
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
forwardRef,
|
|
3
|
-
type CSSProperties,
|
|
4
|
-
type HTMLAttributes,
|
|
5
|
-
type MouseEvent,
|
|
6
|
-
type ReactNode,
|
|
7
|
-
} from "react";
|
|
8
|
-
|
|
9
|
-
import { cn } from "../lib/cn";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Chip — a compact pill representing one selected value (a recipient,
|
|
13
|
-
* a filter, a tag the user picked). Exists so multi-value inputs
|
|
14
|
-
* (MultiCombobox) and token lists share one removable-item shape
|
|
15
|
-
* instead of each product hand-rolling a "name ×" span.
|
|
16
|
-
*
|
|
17
|
-
* Distinct from `Tag` (uppercase mono STATUS label — "PENDING",
|
|
18
|
-
* "LIVE"): a Chip is sentence-case body text (13px sans), a pill on a
|
|
19
|
-
* muted slab, and is interactive — optional `onClick` turns the label
|
|
20
|
-
* into a button (e.g. "edit this entry"), optional `onRemove` adds
|
|
21
|
-
* the × button.
|
|
22
|
-
*
|
|
23
|
-
* Tones: `neutral` (default), `accent`, `warning` (e.g. "needs a
|
|
24
|
-
* name"), `critical`. `selected` outlines the chip in the accent
|
|
25
|
-
* colour (e.g. the entry currently being edited). `disabled` dims the
|
|
26
|
-
* chip and disables both buttons.
|
|
27
|
-
*
|
|
28
|
-
* Compose with: MultiCombobox (entry row), filter bars, recipient
|
|
29
|
-
* lists on detail pages.
|
|
30
|
-
*
|
|
31
|
-
* Visual: see docs/components/multi-combobox.
|
|
32
|
-
*
|
|
33
|
-
* Forwards ref to the root <span>. Spreads `...rest` onto it so
|
|
34
|
-
* consumers can pass data-* / aria-* / title / style. Chrome presets
|
|
35
|
-
* can restyle via the `[data-ck-chip]` / `data-tone` / `data-selected`
|
|
36
|
-
* hooks — 0.24.0 ships no per-chrome CSS for it.
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
export type ChipTone = "neutral" | "accent" | "warning" | "critical";
|
|
40
|
-
|
|
41
|
-
export type ChipProps = Omit<HTMLAttributes<HTMLSpanElement>, "onClick"> & {
|
|
42
|
-
children: ReactNode;
|
|
43
|
-
tone?: ChipTone | undefined;
|
|
44
|
-
// Outline the chip in the accent colour (e.g. "being edited").
|
|
45
|
-
selected?: boolean | undefined;
|
|
46
|
-
// Dim + disable both the label button and the remove button.
|
|
47
|
-
disabled?: boolean | undefined;
|
|
48
|
-
// When set, the label renders as a <button> (e.g. click-to-edit).
|
|
49
|
-
onClick?: (() => void) | undefined;
|
|
50
|
-
// When set, a × button renders after the label.
|
|
51
|
-
onRemove?: (() => void) | undefined;
|
|
52
|
-
// aria-label for the × button. Default "Remove".
|
|
53
|
-
removeLabel?: string | undefined;
|
|
54
|
-
className?: string | undefined;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const TONE_FG: Record<ChipTone, string> = {
|
|
58
|
-
neutral: "var(--ck-text-primary, #111)",
|
|
59
|
-
accent: "var(--ck-accent, #4f46e5)",
|
|
60
|
-
warning: "var(--ck-warning, #D97706)",
|
|
61
|
-
critical: "var(--ck-critical, #DC2626)",
|
|
62
|
-
};
|
|
63
|
-
const TONE_BG: Record<ChipTone, string> = {
|
|
64
|
-
neutral: "var(--ck-bg-muted, #E5E8EE)",
|
|
65
|
-
accent: "var(--ck-accent-muted, rgba(79, 70, 229, 0.08))",
|
|
66
|
-
warning: "var(--ck-warning-muted, #FEF3E7)",
|
|
67
|
-
critical: "var(--ck-critical-muted, #FCEAEA)",
|
|
68
|
-
};
|
|
69
|
-
const TONE_BORDER: Record<ChipTone, string> = {
|
|
70
|
-
neutral: "var(--ck-border-subtle, #DDE0E6)",
|
|
71
|
-
accent: "var(--ck-accent-border, rgba(79, 70, 229, 0.35))",
|
|
72
|
-
warning: "color-mix(in oklab, var(--ck-warning, #D97706) 35%, transparent)",
|
|
73
|
-
critical: "color-mix(in oklab, var(--ck-critical, #DC2626) 35%, transparent)",
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
// Label text styles — shared by the <span> and <button> renderings so
|
|
77
|
-
// the chip looks identical whether or not it is clickable.
|
|
78
|
-
const LABEL_STYLE: CSSProperties = {
|
|
79
|
-
minWidth: 0,
|
|
80
|
-
overflow: "hidden",
|
|
81
|
-
textOverflow: "ellipsis",
|
|
82
|
-
whiteSpace: "nowrap",
|
|
83
|
-
font: "inherit",
|
|
84
|
-
color: "inherit",
|
|
85
|
-
background: "none",
|
|
86
|
-
border: "none",
|
|
87
|
-
padding: 0,
|
|
88
|
-
margin: 0,
|
|
89
|
-
textAlign: "left",
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
export const Chip = forwardRef<HTMLSpanElement, ChipProps>(function Chip(
|
|
93
|
-
{
|
|
94
|
-
children,
|
|
95
|
-
tone = "neutral",
|
|
96
|
-
selected = false,
|
|
97
|
-
disabled = false,
|
|
98
|
-
onClick,
|
|
99
|
-
onRemove,
|
|
100
|
-
removeLabel,
|
|
101
|
-
className,
|
|
102
|
-
style,
|
|
103
|
-
...rest
|
|
104
|
-
},
|
|
105
|
-
ref,
|
|
106
|
-
) {
|
|
107
|
-
const clickable = !!onClick && !disabled;
|
|
108
|
-
// Chip buttons act on click but must NOT take focus on mousedown:
|
|
109
|
-
// when a host has an inline editor open for another chip, the
|
|
110
|
-
// editor's blur (commit + unmount) would otherwise run before this
|
|
111
|
-
// click lands and the click is lost. Keyboard focus via Tab is
|
|
112
|
-
// unaffected.
|
|
113
|
-
const keepFocus = (e: MouseEvent<HTMLButtonElement>): void => e.preventDefault();
|
|
114
|
-
return (
|
|
115
|
-
<span
|
|
116
|
-
ref={ref}
|
|
117
|
-
className={cn("ck-chip", className)}
|
|
118
|
-
data-ck-chip
|
|
119
|
-
data-tone={tone}
|
|
120
|
-
data-selected={selected ? "true" : undefined}
|
|
121
|
-
data-disabled={disabled ? "true" : undefined}
|
|
122
|
-
style={{
|
|
123
|
-
display: "inline-flex",
|
|
124
|
-
alignItems: "center",
|
|
125
|
-
gap: 4,
|
|
126
|
-
maxWidth: "100%",
|
|
127
|
-
boxSizing: "border-box",
|
|
128
|
-
padding: onRemove ? "3px 6px 3px 10px" : "3px 10px",
|
|
129
|
-
borderRadius: 999,
|
|
130
|
-
background: TONE_BG[tone],
|
|
131
|
-
border: `1px solid ${selected ? "var(--ck-accent, #4f46e5)" : TONE_BORDER[tone]}`,
|
|
132
|
-
boxShadow: selected ? "0 0 0 2px var(--ck-accent-muted, rgba(79, 70, 229, 0.12))" : "none",
|
|
133
|
-
font: "500 13px/1.3 var(--ck-font-sans, system-ui, sans-serif)",
|
|
134
|
-
color: TONE_FG[tone],
|
|
135
|
-
opacity: disabled ? 0.5 : 1,
|
|
136
|
-
cursor: disabled ? "default" : undefined,
|
|
137
|
-
transition: "border-color var(--ck-dur-fast, 120ms) var(--ck-ease, ease)",
|
|
138
|
-
...style,
|
|
139
|
-
}}
|
|
140
|
-
{...rest}
|
|
141
|
-
>
|
|
142
|
-
{onClick ? (
|
|
143
|
-
<button
|
|
144
|
-
type="button"
|
|
145
|
-
className="ck-chip-label"
|
|
146
|
-
onMouseDown={keepFocus}
|
|
147
|
-
onClick={onClick}
|
|
148
|
-
disabled={disabled}
|
|
149
|
-
style={{ ...LABEL_STYLE, cursor: clickable ? "pointer" : "default" }}
|
|
150
|
-
>
|
|
151
|
-
{children}
|
|
152
|
-
</button>
|
|
153
|
-
) : (
|
|
154
|
-
<span className="ck-chip-label" style={LABEL_STYLE}>
|
|
155
|
-
{children}
|
|
156
|
-
</span>
|
|
157
|
-
)}
|
|
158
|
-
{onRemove ? (
|
|
159
|
-
<button
|
|
160
|
-
type="button"
|
|
161
|
-
className="ck-chip-remove"
|
|
162
|
-
aria-label={removeLabel ?? "Remove"}
|
|
163
|
-
onMouseDown={keepFocus}
|
|
164
|
-
onClick={onRemove}
|
|
165
|
-
disabled={disabled}
|
|
166
|
-
style={{
|
|
167
|
-
display: "inline-flex",
|
|
168
|
-
alignItems: "center",
|
|
169
|
-
justifyContent: "center",
|
|
170
|
-
width: 16,
|
|
171
|
-
height: 16,
|
|
172
|
-
flexShrink: 0,
|
|
173
|
-
padding: 0,
|
|
174
|
-
margin: 0,
|
|
175
|
-
border: "none",
|
|
176
|
-
borderRadius: 999,
|
|
177
|
-
background: "none",
|
|
178
|
-
color: "inherit",
|
|
179
|
-
opacity: 0.7,
|
|
180
|
-
font: "inherit",
|
|
181
|
-
fontSize: 14,
|
|
182
|
-
lineHeight: 1,
|
|
183
|
-
cursor: disabled ? "default" : "pointer",
|
|
184
|
-
}}
|
|
185
|
-
>
|
|
186
|
-
×
|
|
187
|
-
</button>
|
|
188
|
-
) : null}
|
|
189
|
-
</span>
|
|
190
|
-
);
|
|
191
|
-
});
|