@ntmats/hc-ui 0.0.0-stage → 0.1.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/LICENSE +5 -0
- package/README.md +26 -2
- package/dist/index.d.ts +62 -0
- package/dist/index.js +143 -0
- package/dist/styles.css +221 -0
- package/package.json +31 -4
package/LICENSE
ADDED
package/README.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ntmats/hc-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Imported as `@hc/ui`: `npm install --save-exact @hc/ui@npm:@ntmats/hc-ui@<version>` (React 18+ is a peer dependency). Experimental 0.x API.
|
|
4
|
+
|
|
5
|
+
React presentation primitives, with React and React DOM supplied by each consumer. The initial contract has no router, data, persistence, icon-library or product-specific dependencies.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Button, Chip, IconButton, SegmentedControl, Skeleton, Surface } from '@hc/ui';
|
|
9
|
+
import '@hc/ui/styles.css';
|
|
10
|
+
import '@hc/tokens/css';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Place `data-hc-ds` and the active `data-theme="light|dark"` on an ancestor. Controls use CSS variables from `@hc/tokens`; they do not set global fonts, resets, page backgrounds or theme state. Provide icons and localized labels from the application.
|
|
14
|
+
|
|
15
|
+
`SegmentedControl` is a controlled, single-selection radiogroup. Tab enters at the selected option, every arrow key moves and selects (radio group pattern), Home/End move to the first/last enabled option, and disabled options are skipped. Its density can be comfortable (38px, HCUI view toggles) or compact (32px). `hideLabels` renders icon-only options at 40px wide; labels remain the accessible names and tooltips. Typography uses the variable-font tokens (`font-weight-control`, `font-stretch-condensed`) and the shared `rond` roundness; load Google Sans Flex with the `wdth` and `ROND` axes. A caller owns persistence and any URL state.
|
|
16
|
+
|
|
17
|
+
`Surface` only supplies a semantic presentational role (`page`, `main`, `content`, `elevated`, or `overlay`). It does not add positioning, z-index, backdrop blur or modal behavior.
|
|
18
|
+
|
|
19
|
+
Styles are not in a cascade layer: unlayered consumer resets (HCUI's `button` rule, Tailwind preflight) would override layered declarations. Customize through props and tokens, not overrides of internal classes.
|
|
20
|
+
|
|
21
|
+
HCUI uses `SegmentedControl` through its `ViewToggle` wrapper. During the bootstrap it compiles the source through Vite/TypeScript aliases; switch to the pinned package once published.
|
|
22
|
+
|
|
23
|
+
`Button` is a pill with `secondary` (default), `primary`, `outline` and `danger` variants, `md`/`lg` sizes, an optional leading `icon` and `fullWidth`. `IconButton` requires `label` (accessible name and tooltip) and comes in 36/40/44px; the 36px size keeps a 44px touch target. `Chip` is a filter/option pill; pass `selected` to make it a toggle (`aria-pressed`), omit it for a plain action. All three read the shared `rond` roundness, default to `type="button"`, forward refs, scale slightly on press, show the shared focus ring and drop motion under `prefers-reduced-motion`.
|
|
24
|
+
|
|
25
|
+
`Skeleton` is a static shape that pulses, hidden from assistive technology; the enclosing region announces loading once. Its base rule has zero specificity (`:where`), so the consumer sizes each shape with `width`/`height` or its own class.
|
|
26
|
+
|
|
27
|
+
License: `UNLICENSED` (HCUI and Arena only). Component state and styles still need validation in Arena before a 1.0.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type ButtonHTMLAttributes, type HTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
export type SurfaceTone = 'page' | 'main' | 'content' | 'elevated' | 'overlay';
|
|
3
|
+
export interface SurfaceProps extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
tone?: SurfaceTone;
|
|
5
|
+
}
|
|
6
|
+
/** Semantic color role only; placement, elevation and overlay behavior stay local. */
|
|
7
|
+
export declare function Surface({ tone, className, ...props }: SurfaceProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export interface SegmentedOption<T extends string> {
|
|
9
|
+
value: T;
|
|
10
|
+
label: string;
|
|
11
|
+
icon?: ReactNode;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface SegmentedControlProps<T extends string> extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
15
|
+
label: string;
|
|
16
|
+
value: T;
|
|
17
|
+
options: readonly SegmentedOption<T>[];
|
|
18
|
+
onChange: (value: T) => void;
|
|
19
|
+
density?: 'comfortable' | 'compact';
|
|
20
|
+
orientation?: 'horizontal' | 'vertical';
|
|
21
|
+
/** Icon-only options: labels stay as accessible names and tooltips. */
|
|
22
|
+
hideLabels?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Controlled, single-choice control with radio semantics and roving focus. */
|
|
25
|
+
export declare function SegmentedControl<T extends string>({ label, value, options, onChange, density, orientation, hideLabels, className, ...props }: SegmentedControlProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
export type ButtonVariant = 'secondary' | 'primary' | 'outline' | 'danger';
|
|
27
|
+
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
28
|
+
variant?: ButtonVariant;
|
|
29
|
+
size?: 'md' | 'lg';
|
|
30
|
+
/** Decorative icon before the label; pass it already sized (18px in HCUI). */
|
|
31
|
+
icon?: ReactNode;
|
|
32
|
+
fullWidth?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** Pill button. The caller supplies the label text and handles the action. */
|
|
35
|
+
export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
36
|
+
export interface IconButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'aria-label'> {
|
|
37
|
+
/** Accessible name and tooltip; required because the button has no text. */
|
|
38
|
+
label: string;
|
|
39
|
+
icon: ReactNode;
|
|
40
|
+
/** 36, 40 or 44px. The 36px size keeps a 44px touch target. */
|
|
41
|
+
size?: 'sm' | 'md' | 'lg';
|
|
42
|
+
tone?: 'primary' | 'secondary';
|
|
43
|
+
}
|
|
44
|
+
/** Round icon-only button (back, close, options). */
|
|
45
|
+
export declare const IconButton: import("react").ForwardRefExoticComponent<IconButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
46
|
+
export interface ChipProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
47
|
+
/** Toggle state for filters; omit for a plain action chip. */
|
|
48
|
+
selected?: boolean;
|
|
49
|
+
icon?: ReactNode;
|
|
50
|
+
}
|
|
51
|
+
/** Filter or option chip. Selected chips are filled with the accent color. */
|
|
52
|
+
export declare const Chip: import("react").ForwardRefExoticComponent<ChipProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
53
|
+
export interface SkeletonProps extends HTMLAttributes<HTMLSpanElement> {
|
|
54
|
+
width?: string | number;
|
|
55
|
+
height?: string | number;
|
|
56
|
+
circle?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Static placeholder shape. It is hidden from assistive technology: the
|
|
60
|
+
* enclosing region announces loading once (role="status" + label).
|
|
61
|
+
*/
|
|
62
|
+
export declare function Skeleton({ width, height, circle, className, style, ...props }: SkeletonProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// src/index.tsx
|
|
2
|
+
import { forwardRef, useRef } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
function Surface({ tone = "content", className, ...props }) {
|
|
5
|
+
return /* @__PURE__ */ jsx("div", { ...props, className: ["hc-ui-surface", className].filter(Boolean).join(" "), "data-tone": tone });
|
|
6
|
+
}
|
|
7
|
+
function SegmentedControl({
|
|
8
|
+
label,
|
|
9
|
+
value,
|
|
10
|
+
options,
|
|
11
|
+
onChange,
|
|
12
|
+
density = "comfortable",
|
|
13
|
+
orientation = "horizontal",
|
|
14
|
+
hideLabels = false,
|
|
15
|
+
className,
|
|
16
|
+
...props
|
|
17
|
+
}) {
|
|
18
|
+
const buttons = useRef([]);
|
|
19
|
+
const selectedIndex = options.findIndex((option) => option.value === value && !option.disabled);
|
|
20
|
+
const tabStop = selectedIndex >= 0 ? selectedIndex : options.findIndex((option) => !option.disabled);
|
|
21
|
+
const move = (index, key) => {
|
|
22
|
+
if (!["ArrowRight", "ArrowLeft", "ArrowDown", "ArrowUp", "Home", "End"].includes(key)) return false;
|
|
23
|
+
const direction = key === "ArrowRight" || key === "ArrowDown" ? 1 : -1;
|
|
24
|
+
let next = key === "Home" ? 0 : key === "End" ? options.length - 1 : index;
|
|
25
|
+
if (key !== "Home" && key !== "End") {
|
|
26
|
+
for (let tries = 0; tries < options.length; tries += 1) {
|
|
27
|
+
next = (next + direction + options.length) % options.length;
|
|
28
|
+
if (!options[next].disabled) break;
|
|
29
|
+
}
|
|
30
|
+
} else if (options[next]?.disabled) {
|
|
31
|
+
const step = key === "Home" ? 1 : -1;
|
|
32
|
+
while (next >= 0 && next < options.length && options[next].disabled) next += step;
|
|
33
|
+
}
|
|
34
|
+
if (next < 0 || next >= options.length || options[next]?.disabled) return true;
|
|
35
|
+
onChange(options[next].value);
|
|
36
|
+
buttons.current[next]?.focus();
|
|
37
|
+
return true;
|
|
38
|
+
};
|
|
39
|
+
return /* @__PURE__ */ jsx(
|
|
40
|
+
"div",
|
|
41
|
+
{
|
|
42
|
+
...props,
|
|
43
|
+
className: ["hc-ui-segmented", className].filter(Boolean).join(" "),
|
|
44
|
+
role: "radiogroup",
|
|
45
|
+
"aria-label": label,
|
|
46
|
+
"aria-orientation": orientation,
|
|
47
|
+
"data-density": density,
|
|
48
|
+
"data-orientation": orientation,
|
|
49
|
+
"data-labels": hideLabels ? "hidden" : void 0,
|
|
50
|
+
children: options.map((option, index) => {
|
|
51
|
+
const selected = option.value === value;
|
|
52
|
+
return /* @__PURE__ */ jsxs(
|
|
53
|
+
"button",
|
|
54
|
+
{
|
|
55
|
+
ref: (element) => {
|
|
56
|
+
buttons.current[index] = element;
|
|
57
|
+
},
|
|
58
|
+
type: "button",
|
|
59
|
+
role: "radio",
|
|
60
|
+
"aria-checked": selected,
|
|
61
|
+
"aria-label": option.label,
|
|
62
|
+
title: hideLabels ? option.label : void 0,
|
|
63
|
+
disabled: option.disabled,
|
|
64
|
+
tabIndex: !option.disabled && index === tabStop ? 0 : -1,
|
|
65
|
+
className: "hc-ui-segmented__option",
|
|
66
|
+
"data-selected": selected || void 0,
|
|
67
|
+
onClick: () => !option.disabled && onChange(option.value),
|
|
68
|
+
onKeyDown: (event) => {
|
|
69
|
+
if (move(index, event.key)) event.preventDefault();
|
|
70
|
+
},
|
|
71
|
+
children: [
|
|
72
|
+
option.icon && /* @__PURE__ */ jsx("span", { className: "hc-ui-segmented__icon", "aria-hidden": "true", children: option.icon }),
|
|
73
|
+
!hideLabels && /* @__PURE__ */ jsx("span", { className: "hc-ui-segmented__label", children: option.label })
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
option.value
|
|
77
|
+
);
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
var classes = (...names) => names.filter(Boolean).join(" ");
|
|
83
|
+
var Button = forwardRef(function Button2({ variant = "secondary", size = "md", icon, fullWidth = false, type = "button", className, children, ...props }, ref) {
|
|
84
|
+
return /* @__PURE__ */ jsxs(
|
|
85
|
+
"button",
|
|
86
|
+
{
|
|
87
|
+
...props,
|
|
88
|
+
ref,
|
|
89
|
+
type,
|
|
90
|
+
className: classes("hc-ui-button", className),
|
|
91
|
+
"data-variant": variant,
|
|
92
|
+
"data-size": size,
|
|
93
|
+
"data-full-width": fullWidth || void 0,
|
|
94
|
+
children: [
|
|
95
|
+
icon && /* @__PURE__ */ jsx("span", { className: "hc-ui-button__icon", "aria-hidden": "true", children: icon }),
|
|
96
|
+
children
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
var IconButton = forwardRef(function IconButton2({ label, icon, size = "md", tone = "primary", type = "button", title, className, ...props }, ref) {
|
|
102
|
+
return /* @__PURE__ */ jsx(
|
|
103
|
+
"button",
|
|
104
|
+
{
|
|
105
|
+
...props,
|
|
106
|
+
ref,
|
|
107
|
+
type,
|
|
108
|
+
"aria-label": label,
|
|
109
|
+
title: title ?? label,
|
|
110
|
+
className: classes("hc-ui-icon-button", className),
|
|
111
|
+
"data-size": size,
|
|
112
|
+
"data-tone": tone,
|
|
113
|
+
children: /* @__PURE__ */ jsx("span", { className: "hc-ui-icon-button__icon", "aria-hidden": "true", children: icon })
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
var Chip = forwardRef(function Chip2({ selected, icon, type = "button", className, children, ...props }, ref) {
|
|
118
|
+
return /* @__PURE__ */ jsxs("button", { ...props, ref, type, "aria-pressed": selected, className: classes("hc-ui-chip", className), children: [
|
|
119
|
+
icon && /* @__PURE__ */ jsx("span", { className: "hc-ui-chip__icon", "aria-hidden": "true", children: icon }),
|
|
120
|
+
children
|
|
121
|
+
] });
|
|
122
|
+
});
|
|
123
|
+
function Skeleton({ width, height, circle = false, className, style, ...props }) {
|
|
124
|
+
const size = { ...width !== void 0 && { width }, ...height !== void 0 && { height }, ...style };
|
|
125
|
+
return /* @__PURE__ */ jsx(
|
|
126
|
+
"span",
|
|
127
|
+
{
|
|
128
|
+
...props,
|
|
129
|
+
"aria-hidden": "true",
|
|
130
|
+
className: classes("hc-ui-skeleton", className),
|
|
131
|
+
"data-shape": circle ? "circle" : void 0,
|
|
132
|
+
style: Object.keys(size).length ? size : void 0
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
export {
|
|
137
|
+
Button,
|
|
138
|
+
Chip,
|
|
139
|
+
IconButton,
|
|
140
|
+
SegmentedControl,
|
|
141
|
+
Skeleton,
|
|
142
|
+
Surface
|
|
143
|
+
};
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/* Scoped to package-owned classes; consumers opt into tokens via data-hc-ds.
|
|
2
|
+
No cascade layer: unlayered consumer resets (HCUI's button rule, Tailwind
|
|
3
|
+
preflight) would otherwise override every component declaration. */
|
|
4
|
+
[data-hc-ds] .hc-ui-surface {
|
|
5
|
+
color: var(--hc-color-text-primary);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
[data-hc-ds] .hc-ui-surface[data-tone='page'] { background: var(--hc-color-bg-page); }
|
|
9
|
+
[data-hc-ds] .hc-ui-surface[data-tone='main'] { background: var(--hc-color-bg-main); }
|
|
10
|
+
[data-hc-ds] .hc-ui-surface[data-tone='content'] {
|
|
11
|
+
background: var(--hc-color-bg-surface);
|
|
12
|
+
border: 1px solid var(--hc-color-border);
|
|
13
|
+
border-radius: var(--hc-radius-md);
|
|
14
|
+
}
|
|
15
|
+
[data-hc-ds] .hc-ui-surface[data-tone='elevated'] {
|
|
16
|
+
background: var(--hc-color-bg-elevated);
|
|
17
|
+
border: 1px solid var(--hc-color-border);
|
|
18
|
+
border-radius: var(--hc-radius-md);
|
|
19
|
+
box-shadow: var(--hc-shadow-card);
|
|
20
|
+
}
|
|
21
|
+
[data-hc-ds] .hc-ui-surface[data-tone='overlay'] {
|
|
22
|
+
background: var(--hc-color-bg-elevated);
|
|
23
|
+
border: 1px solid var(--hc-color-border);
|
|
24
|
+
border-radius: var(--hc-radius-md);
|
|
25
|
+
box-shadow: var(--hc-shadow-float);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Visual reference: HCUI segmented track (3px inset, 2px gap, raised knob). */
|
|
29
|
+
[data-hc-ds] .hc-ui-segmented {
|
|
30
|
+
display: inline-flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 2px;
|
|
33
|
+
max-width: 100%;
|
|
34
|
+
padding: 3px;
|
|
35
|
+
background: var(--hc-color-bg-muted);
|
|
36
|
+
border-radius: var(--hc-radius-full);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
[data-hc-ds] .hc-ui-segmented[data-orientation='vertical'] { flex-direction: column; align-items: stretch; }
|
|
40
|
+
|
|
41
|
+
[data-hc-ds] .hc-ui-segmented__option {
|
|
42
|
+
appearance: none;
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
gap: 6px;
|
|
47
|
+
min-width: 0;
|
|
48
|
+
min-height: 38px;
|
|
49
|
+
margin: 0;
|
|
50
|
+
padding: 0 var(--hc-space-3);
|
|
51
|
+
color: var(--hc-color-text-secondary);
|
|
52
|
+
font-family: var(--hc-font-body);
|
|
53
|
+
font-size: var(--hc-font-size-sm);
|
|
54
|
+
font-weight: var(--hc-font-weight-control);
|
|
55
|
+
font-stretch: var(--hc-font-stretch-condensed);
|
|
56
|
+
font-variation-settings: 'ROND' var(--hc-rond);
|
|
57
|
+
line-height: var(--hc-line-height-md);
|
|
58
|
+
white-space: nowrap;
|
|
59
|
+
background: transparent;
|
|
60
|
+
border: 0;
|
|
61
|
+
border-radius: var(--hc-radius-full);
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
-webkit-tap-highlight-color: transparent;
|
|
64
|
+
touch-action: manipulation;
|
|
65
|
+
/* The variable font weight animates, so the chosen option thickens smoothly. */
|
|
66
|
+
transition: background-color var(--hc-transition-fast), color var(--hc-transition-fast),
|
|
67
|
+
box-shadow var(--hc-transition-fast), font-weight 220ms var(--hc-ease-standard);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
[data-hc-ds] .hc-ui-segmented[data-density='compact'] .hc-ui-segmented__option { min-height: 32px; }
|
|
71
|
+
|
|
72
|
+
[data-hc-ds] .hc-ui-segmented[data-labels='hidden'] .hc-ui-segmented__option {
|
|
73
|
+
width: 40px;
|
|
74
|
+
padding: 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
[data-hc-ds] .hc-ui-segmented__option[data-selected] {
|
|
78
|
+
color: var(--hc-color-text-primary);
|
|
79
|
+
background: var(--hc-color-bg-surface);
|
|
80
|
+
box-shadow: var(--hc-shadow-knob);
|
|
81
|
+
font-weight: var(--hc-font-weight-bold);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
[data-hc-ds] .hc-ui-segmented__option:focus-visible {
|
|
85
|
+
outline: 2px solid var(--hc-focus-ring);
|
|
86
|
+
outline-offset: 2px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
[data-hc-ds] .hc-ui-segmented__option:disabled { opacity: 0.48; cursor: not-allowed; }
|
|
90
|
+
[data-hc-ds] .hc-ui-segmented__icon { display: inline-flex; flex: 0 0 auto; }
|
|
91
|
+
[data-hc-ds] .hc-ui-segmented__icon svg { display: block; width: 18px; height: 18px; }
|
|
92
|
+
[data-hc-ds] .hc-ui-segmented__label { overflow: hidden; text-overflow: ellipsis; }
|
|
93
|
+
|
|
94
|
+
/* Shared by Button, IconButton and Chip: reset, touch and press feedback. */
|
|
95
|
+
[data-hc-ds] :is(.hc-ui-button, .hc-ui-icon-button, .hc-ui-chip) {
|
|
96
|
+
appearance: none;
|
|
97
|
+
position: relative;
|
|
98
|
+
display: inline-flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
margin: 0;
|
|
102
|
+
border: 0;
|
|
103
|
+
border-radius: var(--hc-radius-full);
|
|
104
|
+
font-family: var(--hc-font-body);
|
|
105
|
+
font-variation-settings: 'ROND' var(--hc-rond);
|
|
106
|
+
white-space: nowrap;
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
-webkit-tap-highlight-color: transparent;
|
|
109
|
+
touch-action: manipulation;
|
|
110
|
+
transition-property: transform, background-color, box-shadow, color, opacity;
|
|
111
|
+
transition-duration: var(--hc-dur-short);
|
|
112
|
+
transition-timing-function: var(--hc-ease-standard);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
[data-hc-ds] :is(.hc-ui-button, .hc-ui-icon-button, .hc-ui-chip):active:not(:disabled) { transform: scale(0.97); }
|
|
116
|
+
|
|
117
|
+
[data-hc-ds] :is(.hc-ui-button, .hc-ui-icon-button, .hc-ui-chip):focus-visible {
|
|
118
|
+
outline: 2px solid var(--hc-focus-ring);
|
|
119
|
+
outline-offset: 2px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
[data-hc-ds] :is(.hc-ui-button__icon, .hc-ui-icon-button__icon, .hc-ui-chip__icon) { display: inline-flex; flex: 0 0 auto; }
|
|
123
|
+
[data-hc-ds] :is(.hc-ui-button__icon, .hc-ui-icon-button__icon, .hc-ui-chip__icon) svg { display: block; }
|
|
124
|
+
|
|
125
|
+
/* State layer: translucent veil on hover/press (Button and IconButton). */
|
|
126
|
+
[data-hc-ds] :is(.hc-ui-button, .hc-ui-icon-button) { overflow: hidden; }
|
|
127
|
+
[data-hc-ds] :is(.hc-ui-button, .hc-ui-icon-button)::after {
|
|
128
|
+
content: '';
|
|
129
|
+
position: absolute;
|
|
130
|
+
inset: 0;
|
|
131
|
+
border-radius: inherit;
|
|
132
|
+
background-color: currentColor;
|
|
133
|
+
opacity: 0;
|
|
134
|
+
pointer-events: none;
|
|
135
|
+
transition: opacity var(--hc-dur-short) var(--hc-ease-standard);
|
|
136
|
+
}
|
|
137
|
+
@media (hover: hover) {
|
|
138
|
+
[data-hc-ds] :is(.hc-ui-button, .hc-ui-icon-button):hover:not(:disabled)::after { opacity: 0.05; }
|
|
139
|
+
}
|
|
140
|
+
[data-hc-ds] :is(.hc-ui-button, .hc-ui-icon-button):active:not(:disabled)::after { opacity: 0.09; }
|
|
141
|
+
|
|
142
|
+
/* Button — HCUI btn-base / btn-primary / btn-outline / btn-danger / btn-lg. */
|
|
143
|
+
[data-hc-ds] .hc-ui-button {
|
|
144
|
+
gap: var(--hc-space-2);
|
|
145
|
+
min-height: var(--hc-control-height-md);
|
|
146
|
+
padding: 0 var(--hc-space-5);
|
|
147
|
+
color: var(--hc-color-text-primary);
|
|
148
|
+
background-color: var(--hc-color-bg-muted);
|
|
149
|
+
font-size: var(--hc-font-size-md);
|
|
150
|
+
font-weight: var(--hc-font-weight-semibold);
|
|
151
|
+
font-stretch: var(--hc-font-stretch-button);
|
|
152
|
+
font-variation-settings: 'ROND' var(--hc-rond);
|
|
153
|
+
line-height: var(--hc-line-height-md);
|
|
154
|
+
}
|
|
155
|
+
/* The label narrows slightly on press (wdth axis). */
|
|
156
|
+
[data-hc-ds] .hc-ui-button:active:not(:disabled) { font-stretch: var(--hc-font-stretch-pressed); }
|
|
157
|
+
[data-hc-ds] .hc-ui-button[data-variant='primary'] { color: var(--hc-color-text-inverse); background-color: var(--hc-color-accent); }
|
|
158
|
+
[data-hc-ds] .hc-ui-button[data-variant='outline'] { background-color: transparent; box-shadow: inset 0 0 0 1px var(--hc-color-border-strong); }
|
|
159
|
+
[data-hc-ds] .hc-ui-button[data-variant='danger'] { color: var(--hc-color-text-on-danger); background-color: var(--hc-color-bg-danger); }
|
|
160
|
+
[data-hc-ds] .hc-ui-button[data-size='lg'] { min-height: var(--hc-control-height-lg); padding: 0 var(--hc-space-6); font-size: var(--hc-font-size-base); }
|
|
161
|
+
[data-hc-ds] .hc-ui-button[data-full-width] { width: 100%; }
|
|
162
|
+
[data-hc-ds] .hc-ui-button:disabled { opacity: 0.4; cursor: default; }
|
|
163
|
+
|
|
164
|
+
/* IconButton — HCUI icon-button / close-button. */
|
|
165
|
+
[data-hc-ds] .hc-ui-icon-button {
|
|
166
|
+
flex: 0 0 auto;
|
|
167
|
+
width: 40px;
|
|
168
|
+
height: 40px;
|
|
169
|
+
padding: 0;
|
|
170
|
+
color: var(--hc-color-text-primary);
|
|
171
|
+
background-color: var(--hc-color-bg-muted);
|
|
172
|
+
}
|
|
173
|
+
[data-hc-ds] .hc-ui-icon-button[data-size='sm'] { width: 36px; height: 36px; overflow: visible; }
|
|
174
|
+
[data-hc-ds] .hc-ui-icon-button[data-size='lg'] { width: 44px; height: 44px; }
|
|
175
|
+
[data-hc-ds] .hc-ui-icon-button[data-tone='secondary'] { color: var(--hc-color-text-secondary); }
|
|
176
|
+
/* 36px keeps a 44px touch target; the veil stays clipped to the circle. */
|
|
177
|
+
[data-hc-ds] .hc-ui-icon-button[data-size='sm']::before { content: ''; position: absolute; inset: -4px; }
|
|
178
|
+
[data-hc-ds] .hc-ui-icon-button:disabled { opacity: 0.4; cursor: default; }
|
|
179
|
+
|
|
180
|
+
/* Chip — HCUI chip / chip-active. */
|
|
181
|
+
[data-hc-ds] .hc-ui-chip {
|
|
182
|
+
flex: 0 0 auto;
|
|
183
|
+
gap: 6px;
|
|
184
|
+
min-height: var(--hc-control-height-chip);
|
|
185
|
+
padding: 0 14px;
|
|
186
|
+
color: var(--hc-color-text-primary);
|
|
187
|
+
background-color: var(--hc-color-bg-surface);
|
|
188
|
+
box-shadow: inset 0 0 0 1px var(--hc-color-border-strong);
|
|
189
|
+
font-size: var(--hc-font-size-sm);
|
|
190
|
+
font-weight: var(--hc-font-weight-medium);
|
|
191
|
+
line-height: var(--hc-line-height-sm);
|
|
192
|
+
}
|
|
193
|
+
@media (hover: hover) {
|
|
194
|
+
[data-hc-ds] .hc-ui-chip:hover:not(:disabled) { background-color: var(--hc-color-bg-muted); }
|
|
195
|
+
[data-hc-ds] .hc-ui-chip[aria-pressed='true']:hover:not(:disabled) { background-color: var(--hc-color-accent); }
|
|
196
|
+
}
|
|
197
|
+
[data-hc-ds] .hc-ui-chip[aria-pressed='true'] { color: var(--hc-color-text-inverse); background-color: var(--hc-color-accent); box-shadow: none; }
|
|
198
|
+
[data-hc-ds] .hc-ui-chip:disabled { opacity: 0.4; cursor: default; }
|
|
199
|
+
|
|
200
|
+
/* Skeleton — zero specificity on purpose: the consumer sizes each shape. */
|
|
201
|
+
@keyframes hc-ui-skeleton-pulse {
|
|
202
|
+
0%, 100% { opacity: 1; }
|
|
203
|
+
50% { opacity: 0.55; }
|
|
204
|
+
}
|
|
205
|
+
:where([data-hc-ds] .hc-ui-skeleton) {
|
|
206
|
+
display: block;
|
|
207
|
+
height: 12px;
|
|
208
|
+
max-width: 100%;
|
|
209
|
+
border-radius: var(--hc-radius-xs);
|
|
210
|
+
background-color: var(--hc-color-bg-muted);
|
|
211
|
+
pointer-events: none;
|
|
212
|
+
animation: hc-ui-skeleton-pulse 1.4s ease-in-out infinite;
|
|
213
|
+
}
|
|
214
|
+
:where([data-hc-ds] .hc-ui-skeleton[data-shape='circle']) { border-radius: var(--hc-radius-full); }
|
|
215
|
+
|
|
216
|
+
@media (prefers-reduced-motion: reduce) {
|
|
217
|
+
[data-hc-ds] .hc-ui-segmented__option { transition: none; }
|
|
218
|
+
[data-hc-ds] :is(.hc-ui-button, .hc-ui-icon-button, .hc-ui-chip) { transition: none; }
|
|
219
|
+
[data-hc-ds] :is(.hc-ui-button, .hc-ui-icon-button, .hc-ui-chip):active:not(:disabled) { transform: none; }
|
|
220
|
+
:where([data-hc-ds] .hc-ui-skeleton) { animation: none; }
|
|
221
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ntmats/hc-ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Shared HCUI/Arena React presentation primitives.",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./styles.css": "./dist/styles.css"
|
|
18
|
+
},
|
|
19
|
+
"sideEffects": [
|
|
20
|
+
"**/*.css"
|
|
21
|
+
],
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"react": ">=18",
|
|
24
|
+
"react-dom": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"prepack": "node ../../scripts/build-ui.mjs"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"registry": "https://registry.npmjs.org/",
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|