@behivetech/atoms.card 0.0.0-stage → 0.4.2

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/README.md CHANGED
@@ -1,3 +1,94 @@
1
- # Temporary Holding Version
1
+ # Card
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A surface container with configurable padding and an optional header (icon + title + an `action` slot).
4
+
5
+ ## Usage
6
+
7
+ ```jsx
8
+ import { Card } from "@behivetech/atoms.card";
9
+
10
+ function Example() {
11
+ const onClose = () => alert("closed");
12
+ return (
13
+ <>
14
+ <Card padding="md">Content</Card>
15
+
16
+ <Card title="Settings" padding="md">
17
+ Content
18
+ </Card>
19
+
20
+ <Card
21
+ icon="🧩"
22
+ title="Block"
23
+ action={<button onClick={onClose}>✕</button>}
24
+ padding="md"
25
+ >
26
+ Content
27
+ </Card>
28
+ </>
29
+ );
30
+ }
31
+ ```
32
+
33
+ The `action` slot is freeform — pass a close button, a menu, multiple buttons, or nothing. If `icon`, `title`, and `action` are all omitted, no header is rendered and `children` render directly in the card (unchanged from a plain `Card`). When a header is present, `padding` applies to the body instead of the whole card, so the header can span edge-to-edge with its own fixed padding.
34
+
35
+ ## Heading level
36
+
37
+ `title` renders inside an `h1`–`h6` tag (not a `span`) so it takes its correct place in the page's heading outline — set via `headingLevel`, which defaults to `3`. The default assumes the common case: a page's own `h1`, sections within it titled `h2`, and a `Card` nested inside one of those sections landing at `h3`. Override it when a card sits at a different depth — e.g. `headingLevel={2}` for a card used directly under a page's `h1` with no intervening section:
38
+
39
+ ```jsx
40
+ <Card title="Settings" headingLevel={2} padding="md">
41
+ Content
42
+ </Card>
43
+ ```
44
+
45
+ ## Root element
46
+
47
+ The root renders as a `<section>`, labelled by `title` via `aria-labelledby`, whenever a `title` is given — an HTML5 `<section>` should have an accessible heading, and `title` is exactly that. With no `title` (a bare surface, or a header made up of just `icon`/`action`), the root stays a plain `<div>`, since an unlabelled `<section>` isn't a meaningful landmark.
48
+
49
+ ## Interactive (clickable) cards
50
+
51
+ Pass `onSelect` to turn the body into a real `<button>` — hover/focus affordance plus a centered, stacked content layout — for tile/action-card patterns like a component picker grid:
52
+
53
+ ```jsx
54
+ import { Card } from "@behivetech/atoms.card";
55
+ import { Icon } from "@behivetech/atoms.icon";
56
+
57
+ function Example() {
58
+ const item = { name: "Hero banner" };
59
+ const handleSelect = (selected) => alert(selected.name);
60
+ return (
61
+ <Card padding="md" onSelect={() => handleSelect(item)}>
62
+ <Icon name="box" size="lg" />
63
+ <span>{item.name}</span>
64
+ </Card>
65
+ );
66
+ }
67
+ ```
68
+
69
+ This is the same job MUI's `CardActionArea` does, folded into `Card` itself rather than a separate component — `children` must stay to phrasing content (text, spans, icons), since a native `<button>` can't contain block-level elements like `<div>`. `onSelect` is deliberately distinct from `onClick`: `onClick` is the plain pass-through to the root element (e.g. to stop click propagation on a whole panel), while `onSelect` specifically drives the interactive-body behavior described here.
70
+
71
+ ## Overriding surface styling
72
+
73
+ `background`/`border`/`border-radius` read from `--card-background`/`--card-border`/`--card-radius` custom properties (falling back to the design tokens above). A consumer embedding a `Card` for a different context — a docked panel with no elevation, a modal with a larger radius — can set these on its own wrapping class instead of fighting CSS-module specificity:
74
+
75
+ ```scss
76
+ .my-docked-panel {
77
+ --card-border: none;
78
+ --card-radius: 0;
79
+ }
80
+ ```
81
+
82
+ ## Props
83
+
84
+ | Prop | Type | Default | Description |
85
+ | ---------------- | -------------------------------------- | ------- | --------------------------------------------------------------------------------------------- |
86
+ | `padding` | `none \| sm \| md \| lg` | `none` | Internal padding — applies to the body when a header is present |
87
+ | `icon` | `ReactNode` | — | Optional icon rendered before the title |
88
+ | `title` | `ReactNode` | — | Optional header title |
89
+ | `headingLevel` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `3` | Which heading tag wraps `title` |
90
+ | `action` | `ReactNode` | — | Optional content rendered at the end of the header |
91
+ | `onSelect` | `MouseEventHandler<HTMLButtonElement>` | — | Makes the body a clickable `<button>` with hover/focus affordance and a centered stack layout |
92
+ | `scrollableBody` | `boolean` | `false` | Keeps the header fixed and scrolls only the body; for cards with a bounded height |
93
+ | `children` | `ReactNode` | — | Card content |
94
+ | `className` | `string` | — | Additional class names |
@@ -0,0 +1,48 @@
1
+ import { Card } from "./card.js";
2
+
3
+ export const DefaultCard = () => (
4
+ <Card padding="md">Default card with md padding</Card>
5
+ );
6
+
7
+ export const NoPaddingCard = () => (
8
+ <Card padding="none">Card with no padding</Card>
9
+ );
10
+
11
+ export const SmallPaddingCard = () => (
12
+ <Card padding="sm">Card with sm padding</Card>
13
+ );
14
+
15
+ export const LargePaddingCard = () => (
16
+ <Card padding="lg">Card with lg padding</Card>
17
+ );
18
+
19
+ export const CardWithHeader = () => (
20
+ <Card title="Settings" padding="md">
21
+ Card body content
22
+ </Card>
23
+ );
24
+
25
+ export const CardWithIconAndAction = () => (
26
+ <Card
27
+ icon="🧩"
28
+ title="Block"
29
+ action={<button onClick={() => {}}>✕</button>}
30
+ padding="md"
31
+ >
32
+ Card body content
33
+ </Card>
34
+ );
35
+
36
+ export const InteractiveCard = () => (
37
+ <Card padding="md" onSelect={() => {}}>
38
+ <span>🧩</span>
39
+ <span>Click me</span>
40
+ </Card>
41
+ );
42
+
43
+ export const CardAsPageLevelSection = () => (
44
+ <Card title="Settings" headingLevel={2} padding="md">
45
+ Used directly under a page&rsquo;s h1, with no intervening section — its
46
+ title takes the h2 slot instead of the h3 default.
47
+ </Card>
48
+ );
@@ -0,0 +1,178 @@
1
+ // Material Design 3 outlined card.
2
+ //
3
+ // Modal and Drawer render their panels as a Card and re-theme it through the
4
+ // `--card-background` / `--card-border` / `--card-radius` custom properties
5
+ // rather than by reaching into these classes, so those three stay the only
6
+ // public styling hooks. Each color falls back to the pre-MD3 `--color-*` name,
7
+ // which `cms.base-styles` aliases to the matching MD3 role.
8
+
9
+ .BHT__Card {
10
+ background: var(
11
+ --card-background,
12
+ var(--md-sys-color-surface, var(--color-surface))
13
+ );
14
+ color: var(--md-sys-color-on-surface, var(--color-on-surface));
15
+ border: var(
16
+ --card-border,
17
+ 1px solid var(--md-sys-color-outline-variant, var(--color-border))
18
+ );
19
+ border-radius: var(--card-radius, var(--md-sys-shape-corner-medium, 12px));
20
+ // Grid/flex items default to their content's min-content width, not their
21
+ // track/basis minimum — without this, a Card holding wide content (a long
22
+ // stat value, an action row) forces its container wider instead of shrinking.
23
+ min-width: 0;
24
+
25
+ &__header {
26
+ padding: var(--spacing-4) var(--spacing-6);
27
+ border-bottom: 1px solid
28
+ var(--md-sys-color-outline-variant, var(--color-border));
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: space-between;
32
+ gap: var(--spacing-4);
33
+ }
34
+
35
+ &__heading {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: var(--spacing-2);
39
+ min-width: 0;
40
+ }
41
+
42
+ &__icon {
43
+ display: inline-flex;
44
+ line-height: 1;
45
+ }
46
+
47
+ &__title {
48
+ margin: 0;
49
+ font-family: var(--md-sys-typescale-title-medium-font, var(--font-heading));
50
+ font-size: var(--md-sys-typescale-title-medium-size, 1rem);
51
+ font-weight: var(--md-sys-typescale-title-medium-weight, 500);
52
+ line-height: var(--md-sys-typescale-title-medium-line-height, 1.5rem);
53
+ letter-spacing: var(--md-sys-typescale-title-medium-tracking, 0.0094em);
54
+ color: var(--md-sys-color-on-surface, var(--color-text-heading));
55
+ overflow: hidden;
56
+ text-overflow: ellipsis;
57
+ white-space: nowrap;
58
+ }
59
+
60
+ &__action {
61
+ display: inline-flex;
62
+ align-items: center;
63
+ gap: var(--spacing-2);
64
+ flex-shrink: 0;
65
+ }
66
+
67
+ &__body-padding {
68
+ /* Resets so this works whether it renders as a <div> (static) or a
69
+ <button> (interactive) — the button UA defaults are what actually
70
+ need overriding here. */
71
+ display: block;
72
+ width: 100%;
73
+ padding: 0;
74
+ margin: 0;
75
+ background: none;
76
+ border: none;
77
+ font: inherit;
78
+ color: inherit;
79
+ text-align: inherit;
80
+ }
81
+
82
+ &--interactive {
83
+ &.BHT__Card {
84
+ overflow: hidden;
85
+ }
86
+
87
+ .BHT__Card {
88
+ &__body-padding {
89
+ position: relative;
90
+ isolation: isolate;
91
+ display: flex;
92
+ flex-direction: column;
93
+ align-items: center;
94
+ gap: var(--spacing-2);
95
+ text-align: center;
96
+ cursor: pointer;
97
+
98
+ // State layer: a veil of the content color, as on Button, so hover and
99
+ // press follow the palette instead of swapping to a fixed surface.
100
+ &::after {
101
+ content: "";
102
+ position: absolute;
103
+ inset: 0;
104
+ background: currentColor;
105
+ opacity: 0;
106
+ pointer-events: none;
107
+ transition: opacity var(--md-sys-motion-duration-short-2, 100ms)
108
+ var(--md-sys-motion-easing-standard, ease);
109
+ }
110
+
111
+ &:hover::after {
112
+ opacity: var(--md-sys-state-hover-opacity, 0.08);
113
+ }
114
+
115
+ &:focus-visible::after {
116
+ opacity: var(--md-sys-state-focus-opacity, 0.1);
117
+ }
118
+
119
+ &:active::after {
120
+ opacity: var(--md-sys-state-pressed-opacity, 0.1);
121
+ }
122
+
123
+ // Inset rather than the usual +2px offset: the root's `overflow:
124
+ // hidden` (needed to clip the state layer to the corners) would cut
125
+ // off a ring drawn outside the body.
126
+ &:focus-visible {
127
+ outline: 3px solid
128
+ var(--md-sys-color-secondary, var(--color-border-focus));
129
+ outline-offset: -3px;
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ // Header stays put; only the body scrolls when content exceeds the card's
136
+ // height — for cards given a bounded height (a modal or drawer panel).
137
+ &--scrollable-body {
138
+ &.BHT__Card {
139
+ display: flex;
140
+ flex-direction: column;
141
+ overflow: hidden;
142
+ }
143
+
144
+ .BHT__Card {
145
+ &__body-padding {
146
+ overflow-y: auto;
147
+ flex: 1;
148
+ min-height: 0;
149
+ }
150
+ }
151
+ }
152
+
153
+ // &--padding-none needs no rule — &__body-padding already resets padding to 0 above.
154
+
155
+ &--padding-sm {
156
+ .BHT__Card {
157
+ &__body-padding {
158
+ padding: var(--spacing-3);
159
+ }
160
+ }
161
+ }
162
+
163
+ &--padding-md {
164
+ .BHT__Card {
165
+ &__body-padding {
166
+ padding: var(--spacing-5);
167
+ }
168
+ }
169
+ }
170
+
171
+ &--padding-lg {
172
+ .BHT__Card {
173
+ &__body-padding {
174
+ padding: var(--spacing-8);
175
+ }
176
+ }
177
+ }
178
+ }
package/card.tsx ADDED
@@ -0,0 +1,116 @@
1
+ import {
2
+ useId,
3
+ type HTMLAttributes,
4
+ type MouseEventHandler,
5
+ type ReactNode,
6
+ } from "react";
7
+ import { getClassName } from "@bht-component-registry/class-names";
8
+ import styles from "./card.module.scss";
9
+
10
+ export type CardPadding = "none" | "sm" | "md" | "lg";
11
+ export type CardHeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
12
+
13
+ export interface CardProps extends Omit<
14
+ HTMLAttributes<HTMLDivElement>,
15
+ "title" | "onSelect"
16
+ > {
17
+ /** Controls internal padding; defaults to none so consumers opt in. Applies to the body when a header is present. */
18
+ padding?: CardPadding;
19
+ /** Optional icon rendered before the title in the header */
20
+ icon?: ReactNode;
21
+ /** Optional header title; omit along with `icon`/`action` to render no header */
22
+ title?: ReactNode;
23
+ /**
24
+ * Which heading tag (`h1`–`h6`) wraps `title`, so the card's title takes its
25
+ * correct place in the page's heading outline. Defaults to `3`, matching a
26
+ * card nested one level below a page's `h1` title and a `section`'s `h2` —
27
+ * override per-instance when a card sits at a different depth.
28
+ */
29
+ headingLevel?: CardHeadingLevel;
30
+ /** Optional content rendered at the end of the header (buttons, menus, a close control, etc.) */
31
+ action?: ReactNode;
32
+ /**
33
+ * Makes the body a real `<button>` — hover/focus affordance plus a centered,
34
+ * stacked content layout — for tile/action-card patterns (a picker grid,
35
+ * for example) instead of a static surface. Distinct from `onClick` (a plain
36
+ * pass-through to the root element, e.g. for stopping click propagation).
37
+ * Omit for a plain, non-interactive card.
38
+ */
39
+ onSelect?: MouseEventHandler<HTMLButtonElement>;
40
+ /**
41
+ * Keeps the header fixed and scrolls only the body when content exceeds the
42
+ * card's height. Only meaningful when the card has a bounded height (a
43
+ * modal or drawer panel, for example).
44
+ */
45
+ scrollableBody?: boolean;
46
+ /** Additional class names to merge with the component root element */
47
+ className?: string;
48
+ /** Card body content */
49
+ children?: ReactNode;
50
+ }
51
+
52
+ export const Card = ({
53
+ padding = "none",
54
+ icon,
55
+ title,
56
+ headingLevel = 3,
57
+ action,
58
+ onSelect,
59
+ scrollableBody = false,
60
+ className,
61
+ children,
62
+ ...rest
63
+ }: CardProps) => {
64
+ const generatedTitleId = useId();
65
+ const hasHeader =
66
+ icon !== undefined || title !== undefined || action !== undefined;
67
+ const hasTitle = title !== undefined;
68
+ const interactive = onSelect !== undefined;
69
+ const HeadingTag = `h${headingLevel}` as React.ElementType;
70
+ // A section with no accessible name is not a meaningful landmark, so only
71
+ // use <section> when there's an actual heading (title) to name it with.
72
+ const RootTag = (hasTitle ? "section" : "div") as React.ElementType;
73
+ const titleId = hasTitle ? `${generatedTitleId}-title` : undefined;
74
+
75
+ const [rootClass, getChildClass] = getClassName({
76
+ className,
77
+ rootClass: "BHT__Card",
78
+ modifiers: {
79
+ [`padding-${padding}`]: true,
80
+ interactive,
81
+ "scrollable-body": scrollableBody,
82
+ },
83
+ styles,
84
+ });
85
+
86
+ const BodyTag = (interactive ? "button" : "div") as React.ElementType;
87
+
88
+ return (
89
+ <RootTag {...rest} className={rootClass} aria-labelledby={titleId}>
90
+ {hasHeader && (
91
+ <div className={getChildClass("header")}>
92
+ <span className={getChildClass("heading")}>
93
+ {icon !== undefined && (
94
+ <span className={getChildClass("icon")}>{icon}</span>
95
+ )}
96
+ {hasTitle && (
97
+ <HeadingTag id={titleId} className={getChildClass("title")}>
98
+ {title}
99
+ </HeadingTag>
100
+ )}
101
+ </span>
102
+ {action !== undefined && (
103
+ <span className={getChildClass("action")}>{action}</span>
104
+ )}
105
+ </div>
106
+ )}
107
+ <BodyTag
108
+ type={interactive ? "button" : undefined}
109
+ onClick={onSelect}
110
+ className={getChildClass("body-padding")}
111
+ >
112
+ {children}
113
+ </BodyTag>
114
+ </RootTag>
115
+ );
116
+ };
package/dist/index.cjs ADDED
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // ../../../../node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js
34
+ var require_classnames = __commonJS({
35
+ "../../../../node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js"(exports2, module2) {
36
+ "use strict";
37
+ (function() {
38
+ "use strict";
39
+ var hasOwn = {}.hasOwnProperty;
40
+ function classNames() {
41
+ var classes = "";
42
+ for (var i = 0; i < arguments.length; i++) {
43
+ var arg = arguments[i];
44
+ if (arg) {
45
+ classes = appendClass(classes, parseValue(arg));
46
+ }
47
+ }
48
+ return classes;
49
+ }
50
+ function parseValue(arg) {
51
+ if (typeof arg === "string" || typeof arg === "number") {
52
+ return arg;
53
+ }
54
+ if (typeof arg !== "object") {
55
+ return "";
56
+ }
57
+ if (Array.isArray(arg)) {
58
+ return classNames.apply(null, arg);
59
+ }
60
+ if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes("[native code]")) {
61
+ return arg.toString();
62
+ }
63
+ var classes = "";
64
+ for (var key in arg) {
65
+ if (hasOwn.call(arg, key) && arg[key]) {
66
+ classes = appendClass(classes, key);
67
+ }
68
+ }
69
+ return classes;
70
+ }
71
+ function appendClass(value, newClass) {
72
+ if (!newClass) {
73
+ return value;
74
+ }
75
+ if (value) {
76
+ return value + " " + newClass;
77
+ }
78
+ return value + newClass;
79
+ }
80
+ if (typeof module2 !== "undefined" && module2.exports) {
81
+ classNames.default = classNames;
82
+ module2.exports = classNames;
83
+ } else if (typeof define === "function" && typeof define.amd === "object" && define.amd) {
84
+ define("classnames", [], function() {
85
+ return classNames;
86
+ });
87
+ } else {
88
+ window.classNames = classNames;
89
+ }
90
+ })();
91
+ }
92
+ });
93
+
94
+ // index.ts
95
+ var index_exports = {};
96
+ __export(index_exports, {
97
+ Card: () => Card
98
+ });
99
+ module.exports = __toCommonJS(index_exports);
100
+
101
+ // card.tsx
102
+ var import_react = require("react");
103
+
104
+ // ../../../../packages/registry/class-names/dist/index.js
105
+ var import_classnames = __toESM(require_classnames(), 1);
106
+ var getClassName = ({
107
+ className,
108
+ modifiers = {},
109
+ rootClass,
110
+ styles = {}
111
+ }) => {
112
+ function getModifiers() {
113
+ const updatedModifiers = {};
114
+ const modifierKeys = Object.keys(modifiers);
115
+ if (modifierKeys.length) {
116
+ modifierKeys.forEach((modKey) => {
117
+ const classKey = `${rootClass}--${modKey}`;
118
+ const newClassName = styles[classKey] || classKey;
119
+ updatedModifiers[newClassName] = modifiers[modKey] ?? false;
120
+ });
121
+ }
122
+ return updatedModifiers;
123
+ }
124
+ function getChildClass(childClassName) {
125
+ const classKey = `${rootClass}__${childClassName}`;
126
+ return styles[classKey] || classKey;
127
+ }
128
+ function getRootStyles() {
129
+ return (0, import_classnames.default)(
130
+ className,
131
+ {
132
+ ...getModifiers()
133
+ },
134
+ styles[rootClass] || rootClass
135
+ );
136
+ }
137
+ return [getRootStyles(), getChildClass];
138
+ };
139
+
140
+ // card.module.scss
141
+ var card_module_default = {
142
+ "BHT__Card": "BHT__Card",
143
+ "BHT__Card__header": "BHT__Card__header",
144
+ "BHT__Card__heading": "BHT__Card__heading",
145
+ "BHT__Card__icon": "BHT__Card__icon",
146
+ "BHT__Card__title": "BHT__Card__title",
147
+ "BHT__Card__action": "BHT__Card__action",
148
+ "BHT__Card__body-padding": "BHT__Card__body-padding",
149
+ "BHT__Card--interactive": "BHT__Card--interactive",
150
+ "BHT__Card--scrollable-body": "BHT__Card--scrollable-body",
151
+ "BHT__Card--padding-sm": "BHT__Card--padding-sm",
152
+ "BHT__Card--padding-md": "BHT__Card--padding-md",
153
+ "BHT__Card--padding-lg": "BHT__Card--padding-lg"
154
+ };
155
+
156
+ // card.tsx
157
+ var import_jsx_runtime = require("react/jsx-runtime");
158
+ var Card = ({
159
+ padding = "none",
160
+ icon,
161
+ title,
162
+ headingLevel = 3,
163
+ action,
164
+ onSelect,
165
+ scrollableBody = false,
166
+ className,
167
+ children,
168
+ ...rest
169
+ }) => {
170
+ const generatedTitleId = (0, import_react.useId)();
171
+ const hasHeader = icon !== void 0 || title !== void 0 || action !== void 0;
172
+ const hasTitle = title !== void 0;
173
+ const interactive = onSelect !== void 0;
174
+ const HeadingTag = `h${headingLevel}`;
175
+ const RootTag = hasTitle ? "section" : "div";
176
+ const titleId = hasTitle ? `${generatedTitleId}-title` : void 0;
177
+ const [rootClass, getChildClass] = getClassName({
178
+ className,
179
+ rootClass: "BHT__Card",
180
+ modifiers: {
181
+ [`padding-${padding}`]: true,
182
+ interactive,
183
+ "scrollable-body": scrollableBody
184
+ },
185
+ styles: card_module_default
186
+ });
187
+ const BodyTag = interactive ? "button" : "div";
188
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RootTag, { ...rest, className: rootClass, "aria-labelledby": titleId, children: [
189
+ hasHeader && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: getChildClass("header"), children: [
190
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: getChildClass("heading"), children: [
191
+ icon !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: getChildClass("icon"), children: icon }),
192
+ hasTitle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HeadingTag, { id: titleId, className: getChildClass("title"), children: title })
193
+ ] }),
194
+ action !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: getChildClass("action"), children: action })
195
+ ] }),
196
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
197
+ BodyTag,
198
+ {
199
+ type: interactive ? "button" : void 0,
200
+ onClick: onSelect,
201
+ className: getChildClass("body-padding"),
202
+ children
203
+ }
204
+ )
205
+ ] });
206
+ };
207
+ // Annotate the CommonJS export names for ESM import in node:
208
+ 0 && (module.exports = {
209
+ Card
210
+ });
211
+ /*! Bundled license information:
212
+
213
+ classnames/index.js:
214
+ (*!
215
+ Copyright (c) 2018 Jed Watson.
216
+ Licensed under the MIT License (MIT), see
217
+ http://jedwatson.github.io/classnames
218
+ *)
219
+ */
package/dist/index.css ADDED
@@ -0,0 +1,111 @@
1
+ @charset "UTF-8";
2
+
3
+ /* esbuild-sass-plugin:css-chunkcard.module.scss */
4
+ .BHT__Card {
5
+ background: var(--card-background, var(--md-sys-color-surface, var(--color-surface)));
6
+ color: var(--md-sys-color-on-surface, var(--color-on-surface));
7
+ border: var(--card-border, 1px solid var(--md-sys-color-outline-variant, var(--color-border)));
8
+ border-radius: var(--card-radius, var(--md-sys-shape-corner-medium, 12px));
9
+ min-width: 0;
10
+ }
11
+ .BHT__Card__header {
12
+ padding: var(--spacing-4) var(--spacing-6);
13
+ border-bottom: 1px solid var(--md-sys-color-outline-variant, var(--color-border));
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: space-between;
17
+ gap: var(--spacing-4);
18
+ }
19
+ .BHT__Card__heading {
20
+ display: flex;
21
+ align-items: center;
22
+ gap: var(--spacing-2);
23
+ min-width: 0;
24
+ }
25
+ .BHT__Card__icon {
26
+ display: inline-flex;
27
+ line-height: 1;
28
+ }
29
+ .BHT__Card__title {
30
+ margin: 0;
31
+ font-family: var(--md-sys-typescale-title-medium-font, var(--font-heading));
32
+ font-size: var(--md-sys-typescale-title-medium-size, 1rem);
33
+ font-weight: var(--md-sys-typescale-title-medium-weight, 500);
34
+ line-height: var(--md-sys-typescale-title-medium-line-height, 1.5rem);
35
+ letter-spacing: var(--md-sys-typescale-title-medium-tracking, 0.0094em);
36
+ color: var(--md-sys-color-on-surface, var(--color-text-heading));
37
+ overflow: hidden;
38
+ text-overflow: ellipsis;
39
+ white-space: nowrap;
40
+ }
41
+ .BHT__Card__action {
42
+ display: inline-flex;
43
+ align-items: center;
44
+ gap: var(--spacing-2);
45
+ flex-shrink: 0;
46
+ }
47
+ .BHT__Card__body-padding {
48
+ display: block;
49
+ width: 100%;
50
+ padding: 0;
51
+ margin: 0;
52
+ background: none;
53
+ border: none;
54
+ font: inherit;
55
+ color: inherit;
56
+ text-align: inherit;
57
+ }
58
+ .BHT__Card--interactive.BHT__Card {
59
+ overflow: hidden;
60
+ }
61
+ .BHT__Card--interactive .BHT__Card__body-padding {
62
+ position: relative;
63
+ isolation: isolate;
64
+ display: flex;
65
+ flex-direction: column;
66
+ align-items: center;
67
+ gap: var(--spacing-2);
68
+ text-align: center;
69
+ cursor: pointer;
70
+ }
71
+ .BHT__Card--interactive .BHT__Card__body-padding::after {
72
+ content: "";
73
+ position: absolute;
74
+ inset: 0;
75
+ background: currentColor;
76
+ opacity: 0;
77
+ pointer-events: none;
78
+ transition: opacity var(--md-sys-motion-duration-short-2, 100ms) var(--md-sys-motion-easing-standard, ease);
79
+ }
80
+ .BHT__Card--interactive .BHT__Card__body-padding:hover::after {
81
+ opacity: var(--md-sys-state-hover-opacity, 0.08);
82
+ }
83
+ .BHT__Card--interactive .BHT__Card__body-padding:focus-visible::after {
84
+ opacity: var(--md-sys-state-focus-opacity, 0.1);
85
+ }
86
+ .BHT__Card--interactive .BHT__Card__body-padding:active::after {
87
+ opacity: var(--md-sys-state-pressed-opacity, 0.1);
88
+ }
89
+ .BHT__Card--interactive .BHT__Card__body-padding:focus-visible {
90
+ outline: 3px solid var(--md-sys-color-secondary, var(--color-border-focus));
91
+ outline-offset: -3px;
92
+ }
93
+ .BHT__Card--scrollable-body.BHT__Card {
94
+ display: flex;
95
+ flex-direction: column;
96
+ overflow: hidden;
97
+ }
98
+ .BHT__Card--scrollable-body .BHT__Card__body-padding {
99
+ overflow-y: auto;
100
+ flex: 1;
101
+ min-height: 0;
102
+ }
103
+ .BHT__Card--padding-sm .BHT__Card__body-padding {
104
+ padding: var(--spacing-3);
105
+ }
106
+ .BHT__Card--padding-md .BHT__Card__body-padding {
107
+ padding: var(--spacing-5);
108
+ }
109
+ .BHT__Card--padding-lg .BHT__Card__body-padding {
110
+ padding: var(--spacing-8);
111
+ }
@@ -0,0 +1,43 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes, ReactNode, MouseEventHandler } from 'react';
3
+
4
+ type CardPadding = "none" | "sm" | "md" | "lg";
5
+ type CardHeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
6
+ interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title" | "onSelect"> {
7
+ /** Controls internal padding; defaults to none so consumers opt in. Applies to the body when a header is present. */
8
+ padding?: CardPadding;
9
+ /** Optional icon rendered before the title in the header */
10
+ icon?: ReactNode;
11
+ /** Optional header title; omit along with `icon`/`action` to render no header */
12
+ title?: ReactNode;
13
+ /**
14
+ * Which heading tag (`h1`–`h6`) wraps `title`, so the card's title takes its
15
+ * correct place in the page's heading outline. Defaults to `3`, matching a
16
+ * card nested one level below a page's `h1` title and a `section`'s `h2` —
17
+ * override per-instance when a card sits at a different depth.
18
+ */
19
+ headingLevel?: CardHeadingLevel;
20
+ /** Optional content rendered at the end of the header (buttons, menus, a close control, etc.) */
21
+ action?: ReactNode;
22
+ /**
23
+ * Makes the body a real `<button>` — hover/focus affordance plus a centered,
24
+ * stacked content layout — for tile/action-card patterns (a picker grid,
25
+ * for example) instead of a static surface. Distinct from `onClick` (a plain
26
+ * pass-through to the root element, e.g. for stopping click propagation).
27
+ * Omit for a plain, non-interactive card.
28
+ */
29
+ onSelect?: MouseEventHandler<HTMLButtonElement>;
30
+ /**
31
+ * Keeps the header fixed and scrolls only the body when content exceeds the
32
+ * card's height. Only meaningful when the card has a bounded height (a
33
+ * modal or drawer panel, for example).
34
+ */
35
+ scrollableBody?: boolean;
36
+ /** Additional class names to merge with the component root element */
37
+ className?: string;
38
+ /** Card body content */
39
+ children?: ReactNode;
40
+ }
41
+ declare const Card: ({ padding, icon, title, headingLevel, action, onSelect, scrollableBody, className, children, ...rest }: CardProps) => react.JSX.Element;
42
+
43
+ export { Card, type CardHeadingLevel, type CardPadding, type CardProps };
package/dist/index.js ADDED
@@ -0,0 +1,207 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __commonJS = (cb, mod) => function __require() {
8
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
19
+ // If the importer is in node compatibility mode or this is not an ESM
20
+ // file that has been converted to a CommonJS file using a Babel-
21
+ // compatible transform (i.e. "__esModule" has not been set), then set
22
+ // "default" to the CommonJS "module.exports" for node compatibility.
23
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
24
+ mod
25
+ ));
26
+
27
+ // ../../../../node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js
28
+ var require_classnames = __commonJS({
29
+ "../../../../node_modules/.pnpm/classnames@2.5.1/node_modules/classnames/index.js"(exports, module) {
30
+ "use strict";
31
+ (function() {
32
+ "use strict";
33
+ var hasOwn = {}.hasOwnProperty;
34
+ function classNames() {
35
+ var classes = "";
36
+ for (var i = 0; i < arguments.length; i++) {
37
+ var arg = arguments[i];
38
+ if (arg) {
39
+ classes = appendClass(classes, parseValue(arg));
40
+ }
41
+ }
42
+ return classes;
43
+ }
44
+ function parseValue(arg) {
45
+ if (typeof arg === "string" || typeof arg === "number") {
46
+ return arg;
47
+ }
48
+ if (typeof arg !== "object") {
49
+ return "";
50
+ }
51
+ if (Array.isArray(arg)) {
52
+ return classNames.apply(null, arg);
53
+ }
54
+ if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes("[native code]")) {
55
+ return arg.toString();
56
+ }
57
+ var classes = "";
58
+ for (var key in arg) {
59
+ if (hasOwn.call(arg, key) && arg[key]) {
60
+ classes = appendClass(classes, key);
61
+ }
62
+ }
63
+ return classes;
64
+ }
65
+ function appendClass(value, newClass) {
66
+ if (!newClass) {
67
+ return value;
68
+ }
69
+ if (value) {
70
+ return value + " " + newClass;
71
+ }
72
+ return value + newClass;
73
+ }
74
+ if (typeof module !== "undefined" && module.exports) {
75
+ classNames.default = classNames;
76
+ module.exports = classNames;
77
+ } else if (typeof define === "function" && typeof define.amd === "object" && define.amd) {
78
+ define("classnames", [], function() {
79
+ return classNames;
80
+ });
81
+ } else {
82
+ window.classNames = classNames;
83
+ }
84
+ })();
85
+ }
86
+ });
87
+
88
+ // card.tsx
89
+ import {
90
+ useId
91
+ } from "react";
92
+
93
+ // ../../../../packages/registry/class-names/dist/index.js
94
+ var import_classnames = __toESM(require_classnames(), 1);
95
+ var getClassName = ({
96
+ className,
97
+ modifiers = {},
98
+ rootClass,
99
+ styles = {}
100
+ }) => {
101
+ function getModifiers() {
102
+ const updatedModifiers = {};
103
+ const modifierKeys = Object.keys(modifiers);
104
+ if (modifierKeys.length) {
105
+ modifierKeys.forEach((modKey) => {
106
+ const classKey = `${rootClass}--${modKey}`;
107
+ const newClassName = styles[classKey] || classKey;
108
+ updatedModifiers[newClassName] = modifiers[modKey] ?? false;
109
+ });
110
+ }
111
+ return updatedModifiers;
112
+ }
113
+ function getChildClass(childClassName) {
114
+ const classKey = `${rootClass}__${childClassName}`;
115
+ return styles[classKey] || classKey;
116
+ }
117
+ function getRootStyles() {
118
+ return (0, import_classnames.default)(
119
+ className,
120
+ {
121
+ ...getModifiers()
122
+ },
123
+ styles[rootClass] || rootClass
124
+ );
125
+ }
126
+ return [getRootStyles(), getChildClass];
127
+ };
128
+
129
+ // card.module.scss
130
+ var card_module_default = {
131
+ "BHT__Card": "BHT__Card",
132
+ "BHT__Card__header": "BHT__Card__header",
133
+ "BHT__Card__heading": "BHT__Card__heading",
134
+ "BHT__Card__icon": "BHT__Card__icon",
135
+ "BHT__Card__title": "BHT__Card__title",
136
+ "BHT__Card__action": "BHT__Card__action",
137
+ "BHT__Card__body-padding": "BHT__Card__body-padding",
138
+ "BHT__Card--interactive": "BHT__Card--interactive",
139
+ "BHT__Card--scrollable-body": "BHT__Card--scrollable-body",
140
+ "BHT__Card--padding-sm": "BHT__Card--padding-sm",
141
+ "BHT__Card--padding-md": "BHT__Card--padding-md",
142
+ "BHT__Card--padding-lg": "BHT__Card--padding-lg"
143
+ };
144
+
145
+ // card.tsx
146
+ import { jsx, jsxs } from "react/jsx-runtime";
147
+ var Card = ({
148
+ padding = "none",
149
+ icon,
150
+ title,
151
+ headingLevel = 3,
152
+ action,
153
+ onSelect,
154
+ scrollableBody = false,
155
+ className,
156
+ children,
157
+ ...rest
158
+ }) => {
159
+ const generatedTitleId = useId();
160
+ const hasHeader = icon !== void 0 || title !== void 0 || action !== void 0;
161
+ const hasTitle = title !== void 0;
162
+ const interactive = onSelect !== void 0;
163
+ const HeadingTag = `h${headingLevel}`;
164
+ const RootTag = hasTitle ? "section" : "div";
165
+ const titleId = hasTitle ? `${generatedTitleId}-title` : void 0;
166
+ const [rootClass, getChildClass] = getClassName({
167
+ className,
168
+ rootClass: "BHT__Card",
169
+ modifiers: {
170
+ [`padding-${padding}`]: true,
171
+ interactive,
172
+ "scrollable-body": scrollableBody
173
+ },
174
+ styles: card_module_default
175
+ });
176
+ const BodyTag = interactive ? "button" : "div";
177
+ return /* @__PURE__ */ jsxs(RootTag, { ...rest, className: rootClass, "aria-labelledby": titleId, children: [
178
+ hasHeader && /* @__PURE__ */ jsxs("div", { className: getChildClass("header"), children: [
179
+ /* @__PURE__ */ jsxs("span", { className: getChildClass("heading"), children: [
180
+ icon !== void 0 && /* @__PURE__ */ jsx("span", { className: getChildClass("icon"), children: icon }),
181
+ hasTitle && /* @__PURE__ */ jsx(HeadingTag, { id: titleId, className: getChildClass("title"), children: title })
182
+ ] }),
183
+ action !== void 0 && /* @__PURE__ */ jsx("span", { className: getChildClass("action"), children: action })
184
+ ] }),
185
+ /* @__PURE__ */ jsx(
186
+ BodyTag,
187
+ {
188
+ type: interactive ? "button" : void 0,
189
+ onClick: onSelect,
190
+ className: getChildClass("body-padding"),
191
+ children
192
+ }
193
+ )
194
+ ] });
195
+ };
196
+ export {
197
+ Card
198
+ };
199
+ /*! Bundled license information:
200
+
201
+ classnames/index.js:
202
+ (*!
203
+ Copyright (c) 2018 Jed Watson.
204
+ Licensed under the MIT License (MIT), see
205
+ http://jedwatson.github.io/classnames
206
+ *)
207
+ */
package/docs.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "$schema": "https://github.com/behivetech/bht-component-registry/docs.schema.json",
3
+ "version": 2,
4
+ "generatedAt": "2026-10-20T05:49:42.170Z",
5
+ "docs": [
6
+ {
7
+ "displayName": "Card",
8
+ "description": "",
9
+ "extendsNative": true,
10
+ "props": [
11
+ {
12
+ "name": "action",
13
+ "type": "ReactNode",
14
+ "required": false,
15
+ "defaultValue": null,
16
+ "description": "Optional content rendered at the end of the header (buttons, menus, a close control, etc.)"
17
+ },
18
+ {
19
+ "name": "children",
20
+ "type": "ReactNode",
21
+ "required": false,
22
+ "defaultValue": null,
23
+ "description": "Card body content"
24
+ },
25
+ {
26
+ "name": "className",
27
+ "type": "string",
28
+ "required": false,
29
+ "defaultValue": null,
30
+ "description": "Additional class names to merge with the component root element"
31
+ },
32
+ {
33
+ "name": "headingLevel",
34
+ "type": "1 | 2 | 3 | 4 | 5 | 6",
35
+ "required": false,
36
+ "defaultValue": "3",
37
+ "description": "Which heading tag (`h1`–`h6`) wraps `title`, so the card's title takes its\ncorrect place in the page's heading outline. Defaults to `3`, matching a\ncard nested one level below a page's `h1` title and a `section`'s `h2` —\noverride per-instance when a card sits at a different depth."
38
+ },
39
+ {
40
+ "name": "icon",
41
+ "type": "ReactNode",
42
+ "required": false,
43
+ "defaultValue": null,
44
+ "description": "Optional icon rendered before the title in the header"
45
+ },
46
+ {
47
+ "name": "onSelect",
48
+ "type": "MouseEventHandler<HTMLButtonElement>",
49
+ "required": false,
50
+ "defaultValue": null,
51
+ "description": "Makes the body a real `<button>` — hover/focus affordance plus a centered,\nstacked content layout — for tile/action-card patterns (a picker grid,\nfor example) instead of a static surface. Distinct from `onClick` (a plain\npass-through to the root element, e.g. for stopping click propagation).\nOmit for a plain, non-interactive card."
52
+ },
53
+ {
54
+ "name": "padding",
55
+ "type": "\"none\" | \"sm\" | \"md\" | \"lg\"",
56
+ "required": false,
57
+ "defaultValue": "none",
58
+ "description": "Controls internal padding; defaults to none so consumers opt in. Applies to the body when a header is present."
59
+ },
60
+ {
61
+ "name": "scrollableBody",
62
+ "type": "boolean",
63
+ "required": false,
64
+ "defaultValue": "false",
65
+ "description": "Keeps the header fixed and scrolls only the body when content exceeds the\ncard's height. Only meaningful when the card has a bounded height (a\nmodal or drawer panel, for example)."
66
+ },
67
+ {
68
+ "name": "title",
69
+ "type": "ReactNode",
70
+ "required": false,
71
+ "defaultValue": null,
72
+ "description": "Optional header title; omit along with `icon`/`action` to render no header"
73
+ }
74
+ ]
75
+ }
76
+ ],
77
+ "functions": []
78
+ }
package/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { Card } from "./card.js";
2
+ export type { CardProps, CardPadding, CardHeadingLevel } from "./card.js";
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@behivetech/atoms.card",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.4.2",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "private": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs"
12
+ },
13
+ "./styles.css": "./dist/index.css"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "index.ts",
19
+ "card.tsx",
20
+ "card.module.scss",
21
+ "*.example.tsx",
22
+ "docs.json"
23
+ ],
24
+ "registry": {
25
+ "status": "stable",
26
+ "tags": []
27
+ },
28
+ "peerDependencies": {
29
+ "react": "^19.2.7",
30
+ "react-dom": "^19.2.7"
31
+ },
32
+ "devDependencies": {
33
+ "@types/react": "19.2.17",
34
+ "@types/react-dom": "19.2.3",
35
+ "esbuild-sass-plugin": "^3.7.0",
36
+ "eslint": "^9.39.5",
37
+ "postcss-modules": "^9.0.1",
38
+ "tsup": "^8.5.1",
39
+ "typescript": "^6.0.3",
40
+ "vitest": "^4.1.10",
41
+ "@bht-component-registry/class-names": "0.0.6",
42
+ "@bht-component-registry/env": "0.1.0"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/behivetech/bht-galaxy.git",
47
+ "directory": "registry/behivetech/atoms/card"
48
+ },
49
+ "publishConfig": {
50
+ "registry": "https://registry.npmjs.org",
51
+ "access": "public",
52
+ "provenance": true
53
+ },
54
+ "scripts": {
55
+ "build": "tsup && bht-registry-docgen",
56
+ "dev": "tsup --watch",
57
+ "lint": "eslint . --max-warnings 0",
58
+ "check-types": "tsc --noEmit",
59
+ "test": "vitest run"
60
+ }
6
61
  }