@kyzarky/ui 0.2.0 → 0.3.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/README.md CHANGED
@@ -101,6 +101,36 @@ import { List, ListRow, Meter } from "@kyzarky/ui";
101
101
  - **`Meter`** is a bar for a `value` from 0 to 1. It's hidden from screen readers, so
102
102
  state the value in the row's `detail`.
103
103
 
104
+ ```tsx
105
+ import { Button, Card, SegmentedControl } from "@kyzarky/ui";
106
+
107
+ <Card aria-labelledby="progress-heading" className="flex flex-col gap-4">
108
+ <SegmentedControl
109
+ label="Show"
110
+ value={view}
111
+ onChange={setView}
112
+ options={[
113
+ { value: "now", label: "Now" },
114
+ { value: "over-time", label: "Over time" },
115
+ ]}
116
+ />
117
+ <Button type="submit">Check my answer</Button>
118
+ <Button variant="outline" onClick={next}>Next situation</Button>
119
+ </Card>
120
+ ```
121
+
122
+ - **`Card`** is a raised surface, rendered as a `<section>`. Name it with
123
+ `aria-labelledby` or `aria-label`.
124
+ - **`Button`** takes shadcn/ui's props: `variant` is `default` (filled), `outline`, or
125
+ `ghost`, and `size` is `default` or `icon`. Unlike a bare `<button>`, its `type` is
126
+ `button` unless you pass `submit`. It also takes every native button prop.
127
+ - **`SegmentedControl`** picks one option from a few. It's a group of radio buttons
128
+ underneath, so arrow keys move between options. `label` names the group for screen
129
+ readers and isn't shown.
130
+
131
+ `Card` and `Button` take a `className` for layout only (`flex-1`, `w-full`, a `gap`). It's
132
+ added after the component's own classes, not merged with them, so don't use it to restyle.
133
+
104
134
  ## Rules
105
135
 
106
136
  - **Less space inside a group than between groups,** at least 1:2.
@@ -0,0 +1,20 @@
1
+ import type { ComponentProps } from "react";
2
+ declare const VARIANTS: {
3
+ /** Filled with the brand color: the main action. */
4
+ default: string;
5
+ /** An edge and the card color: a second action beside a filled one. */
6
+ outline: string;
7
+ /** Nothing until hovered: a quiet action, such as an icon in a toolbar. */
8
+ ghost: string;
9
+ };
10
+ declare const SIZES: {
11
+ default: string;
12
+ /** Square, for a single icon. Give the button an aria-label. */
13
+ icon: string;
14
+ };
15
+ interface ButtonProps extends ComponentProps<"button"> {
16
+ variant?: keyof typeof VARIANTS;
17
+ size?: keyof typeof SIZES;
18
+ }
19
+ export declare function Button({ variant, size, type, className, ...props }: ButtonProps): import("react").JSX.Element;
20
+ export {};
package/dist/Button.js ADDED
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { joinClasses } from "./joinClasses.js";
3
+ const VARIANTS = {
4
+ /** Filled with the brand color: the main action. */
5
+ default: "bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80",
6
+ /** An edge and the card color: a second action beside a filled one. */
7
+ outline: "border border-input bg-card hover:bg-muted active:bg-muted",
8
+ /** Nothing until hovered: a quiet action, such as an icon in a toolbar. */
9
+ ghost: "hover:bg-muted active:bg-muted",
10
+ };
11
+ const SIZES = {
12
+ default: "px-5",
13
+ /** Square, for a single icon. Give the button an aria-label. */
14
+ icon: "w-11",
15
+ };
16
+ export function Button({ variant = "default", size = "default",
17
+ // A bare <button> inside a form submits it. Submitting should be asked for.
18
+ type = "button", className, ...props }) {
19
+ return (_jsx("button", { type: type, className: joinClasses("inline-flex min-h-11 items-center justify-center gap-2 rounded-xl font-semibold transition-colors focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-2 disabled:pointer-events-none disabled:opacity-50", VARIANTS[variant], SIZES[size], className), ...props }));
20
+ }
package/dist/Card.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { ComponentProps } from "react";
2
+ export declare function Card({ className, ...props }: ComponentProps<"section">): import("react").JSX.Element;
package/dist/Card.js ADDED
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { joinClasses } from "./joinClasses.js";
3
+ export function Card({ className, ...props }) {
4
+ return (_jsx("section", { className: joinClasses("rounded-2xl border border-border bg-card p-5 text-card-foreground shadow-sm", className), ...props }));
5
+ }
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from "react";
2
+ interface SegmentedControlProps<Value extends string> {
3
+ /** What the choice is, for screen readers: "Show", "Sort by". Not displayed. */
4
+ label: string;
5
+ options: readonly {
6
+ value: Value;
7
+ label: ReactNode;
8
+ }[];
9
+ value: Value;
10
+ onChange: (value: Value) => void;
11
+ }
12
+ export declare function SegmentedControl<Value extends string>({ label, options, value, onChange, }: SegmentedControlProps<Value>): import("react").JSX.Element;
13
+ export {};
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // A choice of one option from a few, shown side by side. Underneath it's a group of radio
3
+ // buttons, so arrow keys move between options and a screen reader says how many there
4
+ // are, with no key handling written here. The selected look comes from CSS :has(:checked).
5
+ import { useId } from "react";
6
+ export function SegmentedControl({ label, options, value, onChange, }) {
7
+ // Radio buttons form one group by sharing a name.
8
+ const name = useId();
9
+ return (_jsxs("fieldset", { className: "flex rounded-full bg-muted p-1 text-sm", children: [_jsx("legend", { className: "sr-only", children: label }), options.map((option) => (_jsxs("label", { className: "flex min-h-11 cursor-pointer items-center rounded-full px-4 text-muted-foreground has-checked:bg-card has-checked:font-medium has-checked:text-foreground has-checked:shadow-sm has-focus-visible:outline-2 has-focus-visible:outline-ring has-focus-visible:outline-offset-2", children: [_jsx("input", { type: "radio", className: "sr-only", name: name, value: option.value, checked: option.value === value, onChange: () => onChange(option.value) }), option.label] }, option.value)))] }));
10
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ export { Button } from "./Button.js";
2
+ export { Card } from "./Card.js";
1
3
  export { List } from "./List.js";
2
4
  export { ListRow } from "./ListRow.js";
3
5
  export { Meter } from "./Meter.js";
6
+ export { SegmentedControl } from "./SegmentedControl.js";
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  // The package's components. The tokens they use come from theme.css.
2
+ export { Button } from "./Button.js";
3
+ export { Card } from "./Card.js";
2
4
  export { List } from "./List.js";
3
5
  export { ListRow } from "./ListRow.js";
4
6
  export { Meter } from "./Meter.js";
7
+ export { SegmentedControl } from "./SegmentedControl.js";
@@ -0,0 +1 @@
1
+ export declare function joinClasses(...names: (string | undefined)[]): string;
@@ -0,0 +1,6 @@
1
+ // Joins class names, skipping any that aren't set. It doesn't resolve conflicts: a
2
+ // component's `className` prop is for layout (flex-1, w-full, a margin), which the
3
+ // component's own classes never set.
4
+ export function joinClasses(...names) {
5
+ return names.filter(Boolean).join(" ");
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyzarky/ui",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Design tokens and React components for Ky Merritt's apps, built on Tailwind CSS 4",
5
5
  "license": "MIT",
6
6
  "author": "Ky Merritt",