@kennycorrea/ui 0.6.0 → 0.7.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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0 — 2026-10-09
4
+
5
+ ### Added
6
+ - **`Alert`**: an inline message with `type` (`info` default, `warning`, `success`, `error`,
7
+ `brand`), `head`, `body`, and optional `actionButton` / `secondaryActionButton`
8
+ (`{ label, onPress }`). Same colours and icons as `Toast`; `brand` uses the info icon in the
9
+ theme's own accent. Errors and warnings are `role="alert"`, the rest `role="status"`.
10
+ - **`Skeleton`** (Themes' skeleton) and **`SkeletonText`** (a `lines`-long paragraph placeholder).
11
+
12
+ ## 0.6.1 — 2026-10-08
13
+
14
+ ### Fixed
15
+ - **`Slider` thumbs always have an accessible name.** Without `fillFrom`, `label` landed on the
16
+ Themes root, which has no role, and the `role="slider"` thumb had none (axe:
17
+ aria-input-field-name). The thumb is now named `label`; a range's thumbs are
18
+ `"<label>, minimum"` / `"<label>, maximum"` (`"<label>, n of m"` beyond two) instead of
19
+ Radix's bare "Minimum"/"Maximum". Consumers using `fillFrom={min}` only to get a name can
20
+ drop it.
21
+
3
22
  ## 0.6.0 — 2026-10-07
4
23
 
5
24
  ### Added
@@ -0,0 +1,19 @@
1
+ export type AlertType = 'info' | 'warning' | 'success' | 'error' | 'brand';
2
+ export interface AlertAction {
3
+ label: string;
4
+ onPress?: () => void;
5
+ }
6
+ export interface AlertProps {
7
+ type?: AlertType;
8
+ head?: string;
9
+ body?: string;
10
+ actionButton?: AlertAction;
11
+ secondaryActionButton?: AlertAction;
12
+ className?: string;
13
+ }
14
+ /**
15
+ * An inline message that stays on the page, unlike a toast. Errors and warnings are
16
+ * `role="alert"` so they are announced when they appear; the rest are `status`, which waits
17
+ * its turn rather than interrupting.
18
+ */
19
+ export declare function Alert({ type, head, body, actionButton, secondaryActionButton, className, }: AlertProps): import("react").JSX.Element;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from '../../utils/cn.js';
3
+ import { Button } from '../Button/Button.js';
4
+ import { Icon } from '../Icon/Icon.js';
5
+ /**
6
+ * The same colour and icon vocabulary as `Toast`. `brand` borrows info's glyph but sets no
7
+ * `data-accent-color`, so `--accent-*` is whatever the theme — and the accent picker — say.
8
+ */
9
+ const TYPES = {
10
+ info: { color: 'blue', icon: 'info' },
11
+ warning: { color: 'amber', icon: 'warning' },
12
+ success: { color: 'jade', icon: 'check-circle' },
13
+ error: { color: 'red', icon: 'error' },
14
+ brand: { icon: 'info' },
15
+ };
16
+ /**
17
+ * An inline message that stays on the page, unlike a toast. Errors and warnings are
18
+ * `role="alert"` so they are announced when they appear; the rest are `status`, which waits
19
+ * its turn rather than interrupting.
20
+ */
21
+ export function Alert({ type = 'info', head, body, actionButton, secondaryActionButton, className, }) {
22
+ const { color, icon } = TYPES[type];
23
+ const urgent = type === 'error' || type === 'warning';
24
+ return (_jsxs("div", { role: urgent ? 'alert' : 'status', "data-accent-color": color, "data-alert-type": type, className: cn('flex gap-3 rounded-[var(--radius-3)] border border-[var(--accent-6)] bg-[var(--accent-3)] p-4 text-[var(--gray-12)]', className), children: [_jsx(Icon, { name: icon, className: "mt-0.5 text-[var(--accent-11)]" }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [head && _jsx("p", { className: "font-medium", children: head }), body && _jsx("p", { className: "text-sm text-[var(--gray-11)]", children: body }), (actionButton || secondaryActionButton) && (_jsxs("div", { className: "mt-2 flex flex-wrap gap-2", children: [actionButton && (_jsx(Button, { size: "sm", variant: "primary", onClick: actionButton.onPress, children: actionButton.label })), secondaryActionButton && (_jsx(Button, { size: "sm", variant: "ghost", onClick: secondaryActionButton.onPress, children: secondaryActionButton.label }))] }))] })] }));
25
+ }
@@ -0,0 +1 @@
1
+ export * from './Alert.js';
@@ -0,0 +1 @@
1
+ export * from './Alert.js';
@@ -0,0 +1,19 @@
1
+ import { Skeleton as ThemedSkeleton } from '@radix-ui/themes';
2
+ import type { ComponentPropsWithoutRef } from 'react';
3
+ export interface SkeletonProps extends ComponentPropsWithoutRef<typeof ThemedSkeleton> {
4
+ }
5
+ /**
6
+ * Themes' skeleton, re-exported under the house name.
7
+ *
8
+ * Two ways to use it, both Themes': wrap real content with `loading` and it takes that
9
+ * content's shape, or give it a `width`/`height` and it stands alone. Wrapping is the better
10
+ * one — the placeholder is then exactly the size of what replaces it, so nothing jumps.
11
+ */
12
+ export declare function Skeleton({ className, ...props }: SkeletonProps): import("react").JSX.Element;
13
+ export interface SkeletonTextProps {
14
+ /** How many lines to draw. The last one is shorter, as a paragraph's is. */
15
+ lines?: number;
16
+ className?: string;
17
+ }
18
+ /** A paragraph-shaped placeholder, for where the text's length is not known yet. */
19
+ export declare function SkeletonText({ lines, className }: SkeletonTextProps): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Skeleton as ThemedSkeleton } from '@radix-ui/themes';
3
+ import { cn } from '../../utils/cn.js';
4
+ /**
5
+ * Themes' skeleton, re-exported under the house name.
6
+ *
7
+ * Two ways to use it, both Themes': wrap real content with `loading` and it takes that
8
+ * content's shape, or give it a `width`/`height` and it stands alone. Wrapping is the better
9
+ * one — the placeholder is then exactly the size of what replaces it, so nothing jumps.
10
+ */
11
+ export function Skeleton({ className, ...props }) {
12
+ return _jsx(ThemedSkeleton, { className: cn(className), ...props });
13
+ }
14
+ /** A paragraph-shaped placeholder, for where the text's length is not known yet. */
15
+ export function SkeletonText({ lines = 3, className }) {
16
+ return (_jsx("div", { className: cn('flex flex-col gap-2', className), "aria-hidden": true, children: Array.from({ length: lines }, (_, i) => i).map((i) => (_jsx(ThemedSkeleton, { height: "0.875rem", width: i === lines - 1 && lines > 1 ? '60%' : '100%' }, i))) }));
17
+ }
@@ -0,0 +1 @@
1
+ export * from './Skeleton.js';
@@ -0,0 +1 @@
1
+ export * from './Skeleton.js';
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as SliderPrimitive from '@radix-ui/react-slider';
3
3
  import { Slider as ThemedSlider } from '@radix-ui/themes';
4
- import { forwardRef, useState } from 'react';
4
+ import { forwardRef, useCallback, useLayoutEffect, useRef, useState, } from 'react';
5
5
  import { cn } from '../../utils/cn.js';
6
6
  import { fillEdges, fillStyle, percentOf, thumbCentre } from './geometry.js';
7
7
  const MARGIN_KEYS = ['m', 'mx', 'my', 'mt', 'mr', 'mb', 'ml'];
@@ -30,14 +30,54 @@ export const Slider = forwardRef(function Slider(props, ref) {
30
30
  setInner(next);
31
31
  onValueChange?.(next);
32
32
  };
33
+ // Themes spreads every prop onto the Root, which has no role, and gives its Thumbs no way in —
34
+ // so the role="slider" elements are named here, after Radix has rendered them.
35
+ const themedRoot = useRef(null);
36
+ const setThemedRoot = useCallback((node) => {
37
+ themedRoot.current = node;
38
+ assignRef(ref, node);
39
+ }, [ref]);
40
+ // Radix labels a range's thumbs "Minimum"/"Maximum" in a later render of its own, so a
41
+ // one-off write is overwritten; the observer puts ours back whenever anything touches them.
42
+ useLayoutEffect(() => {
43
+ const root = themedRoot.current;
44
+ if (!root)
45
+ return;
46
+ const name = () => {
47
+ const thumbs = root.querySelectorAll('[role="slider"]');
48
+ thumbs.forEach((thumb, i) => {
49
+ const wanted = thumbLabel(label, i, thumbs.length);
50
+ if (thumb.getAttribute('aria-label') !== wanted)
51
+ thumb.setAttribute('aria-label', wanted);
52
+ });
53
+ };
54
+ name();
55
+ const observer = new MutationObserver(name);
56
+ observer.observe(root, { subtree: true, childList: true, attributeFilter: ['aria-label'] });
57
+ return () => observer.disconnect();
58
+ }, [label]);
33
59
  const unsupported = fillFrom === undefined ? null : fillFromUnsupported(props, values.length);
34
60
  if (unsupported && isDev)
35
61
  console.warn(`Slider: fillFrom is ignored — ${unsupported}.`);
36
- const slider = fillFrom !== undefined && !unsupported ? (_jsx(FillFromSlider, { ref: ref, ...rest, label: label, className: className, values: values, fillFrom: fillFrom, min: min, max: max, onValueChange: handleChange })) : (_jsx(ThemedSlider, { ref: ref, size: "2", "aria-label": label, className: cn(className), ...rest, onValueChange: handleChange }));
62
+ const slider = fillFrom !== undefined && !unsupported ? (_jsx(FillFromSlider, { ref: ref, ...rest, label: label, className: className, values: values, fillFrom: fillFrom, min: min, max: max, onValueChange: handleChange })) : (_jsx(ThemedSlider, { ref: setThemedRoot, size: "2", "aria-label": label, className: cn(className), ...rest, onValueChange: handleChange }));
37
63
  if (!marks?.length)
38
64
  return slider;
39
65
  return (_jsxs("div", { className: "ui-slider-field", children: [_jsx(SliderMarks, { marks: marks, min: min, max: max }), slider] }));
40
66
  });
67
+ /** One thumb takes the label; a range says which end each thumb is. */
68
+ function thumbLabel(label, index, count) {
69
+ if (count === 1)
70
+ return label;
71
+ if (count === 2)
72
+ return `${label}, ${index === 0 ? 'minimum' : 'maximum'}`;
73
+ return `${label}, ${index + 1} of ${count}`;
74
+ }
75
+ function assignRef(ref, value) {
76
+ if (typeof ref === 'function')
77
+ ref(value);
78
+ else if (ref)
79
+ ref.current = value;
80
+ }
41
81
  /**
42
82
  * Radix Themes draws its range itself and offers no way to place it, so this path is built on the
43
83
  * primitives and styled in `tokens.css` (`.ui-slider*`) from Themes' public tokens — never by
@@ -1,3 +1,4 @@
1
+ export * from './Alert/index.js';
1
2
  export * from './Avatar/index.js';
2
3
  export * from './Badge/index.js';
3
4
  export * from './Button/index.js';
@@ -13,6 +14,7 @@ export * from './Progress/index.js';
13
14
  export * from './RichText/index.js';
14
15
  export * from './Select/index.js';
15
16
  export * from './Separator/index.js';
17
+ export * from './Skeleton/index.js';
16
18
  export * from './Slider/index.js';
17
19
  export * from './Spinner/index.js';
18
20
  export * from './Switch/index.js';
@@ -1,3 +1,4 @@
1
+ export * from './Alert/index.js';
1
2
  export * from './Avatar/index.js';
2
3
  export * from './Badge/index.js';
3
4
  export * from './Button/index.js';
@@ -13,6 +14,7 @@ export * from './Progress/index.js';
13
14
  export * from './RichText/index.js';
14
15
  export * from './Select/index.js';
15
16
  export * from './Separator/index.js';
17
+ export * from './Skeleton/index.js';
16
18
  export * from './Slider/index.js';
17
19
  export * from './Spinner/index.js';
18
20
  export * from './Switch/index.js';
@@ -97,5 +97,5 @@ export interface Theming<A extends readonly string[]> {
97
97
  export declare function createTheming<const A extends readonly [string, ...string[]]>(config: ThemingConfig<A>): Theming<A>;
98
98
  export declare const readTheme: () => Theme;
99
99
  export declare const storeTheme: (theme: Theme) => void;
100
- export declare const readAccent: () => "jade" | "red" | "yellow" | "orange" | "purple" | "blue";
101
- export declare const storeAccent: (accent: "jade" | "red" | "yellow" | "orange" | "purple" | "blue") => void;
100
+ export declare const readAccent: () => "red" | "yellow" | "orange" | "purple" | "blue" | "jade";
101
+ export declare const storeAccent: (accent: "red" | "yellow" | "orange" | "purple" | "blue" | "jade") => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kennycorrea/ui",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Upfront's house component layer: atoms over Radix Themes, the token bridge, and the accent logic.",
5
5
  "private": false,
6
6
  "type": "module",