@lo-ink/ui 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.
@@ -0,0 +1,36 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ import { classes } from "./utils.js";
3
+
4
+ export interface EmptyStateProps extends Omit<
5
+ HTMLAttributes<HTMLDivElement>,
6
+ "title"
7
+ > {
8
+ title: ReactNode;
9
+ description?: ReactNode;
10
+ icon?: ReactNode;
11
+ action?: ReactNode;
12
+ }
13
+
14
+ export function EmptyState({
15
+ title,
16
+ description,
17
+ icon,
18
+ action,
19
+ className,
20
+ ...props
21
+ }: EmptyStateProps) {
22
+ return (
23
+ <div {...props} className={classes("lo-ui-empty-state", className)}>
24
+ {icon && (
25
+ <div className="lo-ui-empty-state__icon" aria-hidden="true">
26
+ {icon}
27
+ </div>
28
+ )}
29
+ <h2 className="lo-ui-empty-state__title">{title}</h2>
30
+ {description && (
31
+ <p className="lo-ui-empty-state__description">{description}</p>
32
+ )}
33
+ {action && <div className="lo-ui-empty-state__action">{action}</div>}
34
+ </div>
35
+ );
36
+ }
package/src/Layout.tsx ADDED
@@ -0,0 +1,32 @@
1
+ import type { CSSProperties, HTMLAttributes } from "react";
2
+ import { classes } from "./utils.js";
3
+
4
+ type Gap = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10;
5
+ type LayoutProps = HTMLAttributes<HTMLDivElement> & { gap?: Gap };
6
+
7
+ function layoutStyle(gap: Gap, style?: CSSProperties): CSSProperties {
8
+ return {
9
+ "--lo-layout-gap": `var(--lo-space-${gap})`,
10
+ ...style,
11
+ } as CSSProperties;
12
+ }
13
+
14
+ export function Stack({ gap = 4, className, style, ...props }: LayoutProps) {
15
+ return (
16
+ <div
17
+ {...props}
18
+ className={classes("lo-ui-stack", className)}
19
+ style={layoutStyle(gap, style)}
20
+ />
21
+ );
22
+ }
23
+
24
+ export function Inline({ gap = 2, className, style, ...props }: LayoutProps) {
25
+ return (
26
+ <div
27
+ {...props}
28
+ className={classes("lo-ui-inline", className)}
29
+ style={layoutStyle(gap, style)}
30
+ />
31
+ );
32
+ }
package/src/List.tsx ADDED
@@ -0,0 +1,115 @@
1
+ import { forwardRef, useId, type HTMLAttributes, type ReactNode } from "react";
2
+ import { classes } from "./utils.js";
3
+
4
+ export interface ListProps extends HTMLAttributes<HTMLUListElement> {
5
+ label?: ReactNode;
6
+ }
7
+
8
+ export function List({
9
+ label,
10
+ children,
11
+ className,
12
+ "aria-label": ariaLabel,
13
+ "aria-labelledby": ariaLabelledBy,
14
+ ...props
15
+ }: ListProps) {
16
+ const generatedId = useId();
17
+ const labelId = label ? `${generatedId}-label` : undefined;
18
+ const effectiveLabelledBy =
19
+ ariaLabelledBy ?? (ariaLabel === undefined ? labelId : undefined);
20
+ return (
21
+ <section className="lo-ui-list-section">
22
+ {label && (
23
+ <h2 className="lo-ui-list-label" id={labelId}>
24
+ {label}
25
+ </h2>
26
+ )}
27
+ <ul
28
+ {...props}
29
+ aria-label={ariaLabel}
30
+ aria-labelledby={effectiveLabelledBy}
31
+ className={classes("lo-ui-list", className)}
32
+ >
33
+ {children}
34
+ </ul>
35
+ </section>
36
+ );
37
+ }
38
+
39
+ export interface CellProps extends Omit<
40
+ HTMLAttributes<HTMLLIElement>,
41
+ "title"
42
+ > {
43
+ title: ReactNode;
44
+ subtitle?: ReactNode;
45
+ leading?: ReactNode;
46
+ /** Noninteractive metadata included in the row action and its accessible name. */
47
+ trailing?: ReactNode;
48
+ /** An independently operable control rendered beside the row action. */
49
+ trailingAction?: ReactNode;
50
+ onPress?: () => void;
51
+ disabled?: boolean;
52
+ }
53
+
54
+ export const Cell = forwardRef<HTMLLIElement, CellProps>(function Cell(
55
+ {
56
+ title,
57
+ subtitle,
58
+ leading,
59
+ trailing,
60
+ trailingAction,
61
+ onPress,
62
+ disabled = false,
63
+ className,
64
+ ...props
65
+ },
66
+ ref,
67
+ ) {
68
+ const content = (
69
+ <>
70
+ {leading && <span className="lo-ui-cell__leading">{leading}</span>}
71
+ <span className="lo-ui-cell__body">
72
+ <span className="lo-ui-cell__title">{title}</span>
73
+ {subtitle && <span className="lo-ui-cell__subtitle">{subtitle}</span>}
74
+ </span>
75
+ {trailing && <span className="lo-ui-cell__trailing">{trailing}</span>}
76
+ {onPress && !trailing && !trailingAction && (
77
+ <svg
78
+ className="lo-ui-cell__chevron"
79
+ viewBox="0 0 20 20"
80
+ aria-hidden="true"
81
+ >
82
+ <path d="m7.5 4 6 6-6 6" />
83
+ </svg>
84
+ )}
85
+ </>
86
+ );
87
+
88
+ return (
89
+ <li {...props} ref={ref} className={classes("lo-ui-cell", className)}>
90
+ {onPress || trailingAction ? (
91
+ <div className="lo-ui-cell__actions">
92
+ {onPress ? (
93
+ <button
94
+ className="lo-ui-cell__control"
95
+ type="button"
96
+ onClick={onPress}
97
+ disabled={disabled}
98
+ >
99
+ {content}
100
+ </button>
101
+ ) : (
102
+ <div className="lo-ui-cell__content">{content}</div>
103
+ )}
104
+ {trailingAction && (
105
+ <span className="lo-ui-cell__trailing-action">
106
+ {trailingAction}
107
+ </span>
108
+ )}
109
+ </div>
110
+ ) : (
111
+ <div className="lo-ui-cell__content">{content}</div>
112
+ )}
113
+ </li>
114
+ );
115
+ });
@@ -0,0 +1,128 @@
1
+ import {
2
+ forwardRef,
3
+ useId,
4
+ type InputHTMLAttributes,
5
+ type ReactNode,
6
+ } from "react";
7
+ import { classes } from "./utils.js";
8
+
9
+ export type SelectionControlProps = Omit<
10
+ InputHTMLAttributes<HTMLInputElement>,
11
+ "type"
12
+ > & {
13
+ label: ReactNode;
14
+ description?: ReactNode;
15
+ labelHidden?: boolean;
16
+ };
17
+
18
+ function SelectionControl({
19
+ control,
20
+ label,
21
+ description,
22
+ labelHidden = false,
23
+ id,
24
+ }: {
25
+ control: ReactNode;
26
+ label: ReactNode;
27
+ description?: ReactNode;
28
+ labelHidden?: boolean;
29
+ id: string;
30
+ }) {
31
+ return (
32
+ <label
33
+ className={classes(
34
+ "lo-ui-selection",
35
+ labelHidden && "lo-ui-selection--control-only",
36
+ )}
37
+ htmlFor={id}
38
+ >
39
+ <span className="lo-ui-selection__copy">
40
+ <span className="lo-ui-selection__label" id={`${id}-label`}>
41
+ {label}
42
+ </span>
43
+ {description && (
44
+ <span
45
+ className="lo-ui-selection__description"
46
+ id={`${id}-description`}
47
+ >
48
+ {description}
49
+ </span>
50
+ )}
51
+ </span>
52
+ {control}
53
+ </label>
54
+ );
55
+ }
56
+
57
+ export const Switch = forwardRef<HTMLInputElement, SelectionControlProps>(
58
+ function Switch(
59
+ { label, description, labelHidden, id: providedId, className, ...props },
60
+ ref,
61
+ ) {
62
+ const generatedId = useId();
63
+ const id = providedId ?? generatedId;
64
+ return (
65
+ <SelectionControl
66
+ id={id}
67
+ label={label}
68
+ description={description}
69
+ labelHidden={labelHidden}
70
+ control={
71
+ <input
72
+ {...props}
73
+ ref={ref}
74
+ id={id}
75
+ type="checkbox"
76
+ role="switch"
77
+ aria-labelledby={
78
+ props["aria-labelledby"] ??
79
+ (props["aria-label"] ? undefined : `${id}-label`)
80
+ }
81
+ aria-describedby={
82
+ [props["aria-describedby"], description && `${id}-description`]
83
+ .filter(Boolean)
84
+ .join(" ") || undefined
85
+ }
86
+ className={classes("lo-ui-switch", className)}
87
+ />
88
+ }
89
+ />
90
+ );
91
+ },
92
+ );
93
+
94
+ export const Checkbox = forwardRef<HTMLInputElement, SelectionControlProps>(
95
+ function Checkbox(
96
+ { label, description, labelHidden, id: providedId, className, ...props },
97
+ ref,
98
+ ) {
99
+ const generatedId = useId();
100
+ const id = providedId ?? generatedId;
101
+ return (
102
+ <SelectionControl
103
+ id={id}
104
+ label={label}
105
+ description={description}
106
+ labelHidden={labelHidden}
107
+ control={
108
+ <input
109
+ {...props}
110
+ ref={ref}
111
+ id={id}
112
+ type="checkbox"
113
+ aria-labelledby={
114
+ props["aria-labelledby"] ??
115
+ (props["aria-label"] ? undefined : `${id}-label`)
116
+ }
117
+ aria-describedby={
118
+ [props["aria-describedby"], description && `${id}-description`]
119
+ .filter(Boolean)
120
+ .join(" ") || undefined
121
+ }
122
+ className={classes("lo-ui-checkbox", className)}
123
+ />
124
+ }
125
+ />
126
+ );
127
+ },
128
+ );
@@ -0,0 +1,67 @@
1
+ import {
2
+ forwardRef,
3
+ useId,
4
+ type InputHTMLAttributes,
5
+ type ReactNode,
6
+ } from "react";
7
+ import { classes } from "./utils.js";
8
+
9
+ export interface TextFieldProps extends Omit<
10
+ InputHTMLAttributes<HTMLInputElement>,
11
+ "size"
12
+ > {
13
+ label: ReactNode;
14
+ description?: ReactNode;
15
+ error?: ReactNode;
16
+ inputClassName?: string;
17
+ }
18
+
19
+ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
20
+ function TextField(
21
+ {
22
+ label,
23
+ description,
24
+ error,
25
+ id: providedId,
26
+ className,
27
+ inputClassName,
28
+ "aria-describedby": ariaDescribedBy,
29
+ ...props
30
+ },
31
+ ref,
32
+ ) {
33
+ const generatedId = useId();
34
+ const id = providedId ?? generatedId;
35
+ const descriptionId = description ? `${id}-description` : undefined;
36
+ const errorId = error ? `${id}-error` : undefined;
37
+ const describedBy = [ariaDescribedBy, descriptionId, errorId]
38
+ .filter(Boolean)
39
+ .join(" ");
40
+
41
+ return (
42
+ <div className={classes("lo-ui-field", className)}>
43
+ <label className="lo-ui-field__label" htmlFor={id}>
44
+ {label}
45
+ </label>
46
+ <input
47
+ {...props}
48
+ ref={ref}
49
+ id={id}
50
+ aria-invalid={error ? true : props["aria-invalid"]}
51
+ aria-describedby={describedBy || undefined}
52
+ className={classes("lo-ui-field__input", inputClassName)}
53
+ />
54
+ {description && (
55
+ <div className="lo-ui-field__description" id={descriptionId}>
56
+ {description}
57
+ </div>
58
+ )}
59
+ {error && (
60
+ <div className="lo-ui-field__error" id={errorId}>
61
+ {error}
62
+ </div>
63
+ )}
64
+ </div>
65
+ );
66
+ },
67
+ );
@@ -0,0 +1,33 @@
1
+ import type { HTMLAttributes } from "react";
2
+ import { classes } from "./utils.js";
3
+
4
+ export interface TextProps extends HTMLAttributes<HTMLParagraphElement> {
5
+ tone?: "primary" | "secondary" | "danger";
6
+ size?: "caption" | "label" | "body";
7
+ }
8
+
9
+ export function Text({
10
+ tone = "primary",
11
+ size = "body",
12
+ className,
13
+ ...props
14
+ }: TextProps) {
15
+ return (
16
+ <p
17
+ {...props}
18
+ className={classes(
19
+ "lo-ui-text",
20
+ `lo-ui-text--${tone}`,
21
+ `lo-ui-text--${size}`,
22
+ className,
23
+ )}
24
+ />
25
+ );
26
+ }
27
+
28
+ export function Heading({
29
+ className,
30
+ ...props
31
+ }: HTMLAttributes<HTMLHeadingElement>) {
32
+ return <h1 {...props} className={classes("lo-ui-heading", className)} />;
33
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { AppIcon, type AppIconProps } from "./AppIcon.js";
2
+ export { Button, type ButtonProps } from "./Button.js";
3
+ export { EmptyState, type EmptyStateProps } from "./EmptyState.js";
4
+ export { Inline, Stack } from "./Layout.js";
5
+ export { Cell, List, type CellProps, type ListProps } from "./List.js";
6
+ export {
7
+ Checkbox,
8
+ Switch,
9
+ type SelectionControlProps,
10
+ } from "./SelectionControl.js";
11
+ export { TextField, type TextFieldProps } from "./TextField.js";
12
+ export { Heading, Text, type TextProps } from "./Typography.js";