@lo-ink/ui 0.3.0 → 0.4.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/dist/Surface.d.ts.map +1 -1
- package/dist/Surface.js +2 -2
- package/dist/Tabs.d.ts +15 -0
- package/dist/Tabs.d.ts.map +1 -0
- package/dist/Tabs.js +49 -0
- package/dist/TextField.d.ts +1 -0
- package/dist/TextField.d.ts.map +1 -1
- package/dist/TextField.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/styles.css +63 -4
- package/package.json +3 -3
- package/src/Surface.tsx +3 -7
- package/src/Tabs.tsx +111 -0
- package/src/TextField.tsx +8 -1
- package/src/index.ts +1 -0
- package/src/styles.css +63 -4
package/dist/Surface.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Surface.d.ts","sourceRoot":"","sources":["../src/Surface.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1D,MAAM,WAAW,YAAa,SAAQ,cAAc,CAAC,WAAW,CAAC;IAC/D,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB;AACD,wBAAgB,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"Surface.d.ts","sourceRoot":"","sources":["../src/Surface.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,OAAO,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1D,MAAM,WAAW,YAAa,SAAQ,cAAc,CAAC,WAAW,CAAC;IAC/D,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB;AACD,wBAAgB,OAAO,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,EAAE,YAAY,+BAc5E"}
|
package/dist/Surface.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { classes } from "./utils.js";
|
|
3
|
-
export function Surface({ padding
|
|
3
|
+
export function Surface({ padding, className, style, ...props }) {
|
|
4
4
|
return (_jsx("section", { ...props, className: classes("lo-ui-surface", className), style: {
|
|
5
|
-
"--lo-surface-padding": `var(--lo-space-${padding})`,
|
|
5
|
+
"--lo-surface-padding": padding === undefined ? "10px" : `var(--lo-space-${padding})`,
|
|
6
6
|
...style,
|
|
7
7
|
} }));
|
|
8
8
|
}
|
package/dist/Tabs.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
export interface TabOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: ReactNode;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
panelId?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface TabsProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
9
|
+
options: readonly TabOption[];
|
|
10
|
+
value: string;
|
|
11
|
+
onValueChange: (value: string) => void;
|
|
12
|
+
}
|
|
13
|
+
/** Controlled horizontal tabs. Labels retain their width and scroll into view. */
|
|
14
|
+
export declare function Tabs({ options, value, onValueChange, id: providedId, className, ...props }: TabsProps): import("react").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=Tabs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../src/Tabs.tsx"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,WAAW,SAAU,SAAQ,IAAI,CACrC,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,OAAO,EAAE,SAAS,SAAS,EAAE,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAED,kFAAkF;AAClF,wBAAgB,IAAI,CAAC,EACnB,OAAO,EACP,KAAK,EACL,aAAa,EACb,EAAE,EAAE,UAAU,EACd,SAAS,EACT,GAAG,KAAK,EACT,EAAE,SAAS,+BA8EX"}
|
package/dist/Tabs.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useRef, } from "react";
|
|
3
|
+
import { classes } from "./utils.js";
|
|
4
|
+
/** Controlled horizontal tabs. Labels retain their width and scroll into view. */
|
|
5
|
+
export function Tabs({ options, value, onValueChange, id: providedId, className, ...props }) {
|
|
6
|
+
const generatedId = useId();
|
|
7
|
+
const id = providedId ?? generatedId;
|
|
8
|
+
const row = useRef(null);
|
|
9
|
+
const focusValue = options.some((option) => option.value === value && !option.disabled)
|
|
10
|
+
? value
|
|
11
|
+
: options.find((option) => !option.disabled)?.value;
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const selected = row.current?.querySelector('[aria-selected="true"]');
|
|
14
|
+
if (!selected || !row.current)
|
|
15
|
+
return;
|
|
16
|
+
const viewport = row.current;
|
|
17
|
+
const bounds = selected.getBoundingClientRect();
|
|
18
|
+
const visible = viewport.getBoundingClientRect();
|
|
19
|
+
if (bounds.left < visible.left)
|
|
20
|
+
viewport.scrollLeft += bounds.left - visible.left;
|
|
21
|
+
else if (bounds.right > visible.right)
|
|
22
|
+
viewport.scrollLeft += bounds.right - visible.right;
|
|
23
|
+
}, [value, options]);
|
|
24
|
+
return (_jsx("div", { ...props, id: id, ref: row, role: "tablist", "aria-orientation": "horizontal", className: classes("lo-ui-tabs", className), children: options.map((option, index) => (_jsx("button", { id: `${id}-tab-${index}`, type: "button", role: "tab", "aria-selected": option.value === value, "aria-controls": option.panelId, tabIndex: option.value === focusValue ? 0 : -1, disabled: option.disabled, className: "lo-ui-tab", onClick: () => onValueChange(option.value), onKeyDown: (event) => {
|
|
25
|
+
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key))
|
|
26
|
+
return;
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
const enabled = options
|
|
29
|
+
.map((item, i) => (item.disabled ? -1 : i))
|
|
30
|
+
.filter((i) => i >= 0);
|
|
31
|
+
if (!enabled.length)
|
|
32
|
+
return;
|
|
33
|
+
const current = enabled.indexOf(index);
|
|
34
|
+
const forward = getComputedStyle(event.currentTarget).direction === "rtl"
|
|
35
|
+
? "ArrowLeft"
|
|
36
|
+
: "ArrowRight";
|
|
37
|
+
const next = event.key === "Home"
|
|
38
|
+
? enabled[0]
|
|
39
|
+
: event.key === "End"
|
|
40
|
+
? enabled[enabled.length - 1]
|
|
41
|
+
: enabled[(current +
|
|
42
|
+
(event.key === forward ? 1 : -1) +
|
|
43
|
+
enabled.length) %
|
|
44
|
+
enabled.length];
|
|
45
|
+
onValueChange(options[next].value);
|
|
46
|
+
const buttons = row.current?.querySelectorAll('[role="tab"]');
|
|
47
|
+
buttons?.[next]?.focus();
|
|
48
|
+
}, children: option.label }, option.value))) }));
|
|
49
|
+
}
|
package/dist/TextField.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface TextFieldProps extends Omit<InputHTMLAttributes<HTMLInputElemen
|
|
|
5
5
|
error?: ReactNode;
|
|
6
6
|
inputClassName?: string;
|
|
7
7
|
labelHidden?: boolean;
|
|
8
|
+
variant?: "outlined" | "search";
|
|
8
9
|
}
|
|
9
10
|
export declare const TextField: import("react").ForwardRefExoticComponent<TextFieldProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
10
11
|
//# sourceMappingURL=TextField.d.ts.map
|
package/dist/TextField.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextField.d.ts","sourceRoot":"","sources":["../src/TextField.tsx"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,MAAM,WAAW,cAAe,SAAQ,IAAI,CAC1C,mBAAmB,CAAC,gBAAgB,CAAC,EACrC,MAAM,CACP;IACC,KAAK,EAAE,SAAS,CAAC;IACjB,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"TextField.d.ts","sourceRoot":"","sources":["../src/TextField.tsx"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,MAAM,WAAW,cAAe,SAAQ,IAAI,CAC1C,mBAAmB,CAAC,gBAAgB,CAAC,EACrC,MAAM,CACP;IACC,KAAK,EAAE,SAAS,CAAC;IACjB,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC;CACjC;AAED,eAAO,MAAM,SAAS,6GA6DrB,CAAC"}
|
package/dist/TextField.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useId, } from "react";
|
|
3
3
|
import { classes } from "./utils.js";
|
|
4
|
-
export const TextField = forwardRef(function TextField({ label, description, error, id: providedId, className, inputClassName, labelHidden = false, "aria-describedby": ariaDescribedBy, ...props }, ref) {
|
|
4
|
+
export const TextField = forwardRef(function TextField({ label, description, error, id: providedId, className, inputClassName, labelHidden = false, variant = "outlined", "aria-describedby": ariaDescribedBy, ...props }, ref) {
|
|
5
5
|
const generatedId = useId();
|
|
6
6
|
const id = providedId ?? generatedId;
|
|
7
7
|
const descriptionId = description ? `${id}-description` : undefined;
|
|
@@ -9,5 +9,5 @@ export const TextField = forwardRef(function TextField({ label, description, err
|
|
|
9
9
|
const describedBy = [ariaDescribedBy, descriptionId, errorId]
|
|
10
10
|
.filter(Boolean)
|
|
11
11
|
.join(" ");
|
|
12
|
-
return (_jsxs("div", { className: classes("lo-ui-field", labelHidden && "lo-ui-field--label-hidden", className), children: [_jsx("label", { className: "lo-ui-field__label", htmlFor: id, children: label }), _jsx("input", { ...props, ref: ref, id: id, "aria-invalid": error ? true : props["aria-invalid"], "aria-describedby": describedBy || undefined, className: classes("lo-ui-field__input", inputClassName) }), description && (_jsx("div", { className: "lo-ui-field__description", id: descriptionId, children: description })), error && (_jsx("div", { className: "lo-ui-field__error", id: errorId, children: error }))] }));
|
|
12
|
+
return (_jsxs("div", { className: classes("lo-ui-field", `lo-ui-field--${variant}`, labelHidden && "lo-ui-field--label-hidden", className), children: [_jsx("label", { className: "lo-ui-field__label", htmlFor: id, children: label }), _jsx("input", { ...props, ref: ref, id: id, "aria-invalid": error ? true : props["aria-invalid"], "aria-describedby": describedBy || undefined, className: classes("lo-ui-field__input", variant === "search" && "lo-ui-field__input--search", inputClassName) }), description && (_jsx("div", { className: "lo-ui-field__description", id: descriptionId, children: description })), error && (_jsx("div", { className: "lo-ui-field__error", id: errorId, children: error }))] }));
|
|
13
13
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -10,4 +10,5 @@ export { TextArea, type TextAreaProps } from "./TextArea.js";
|
|
|
10
10
|
export { Surface, type SurfaceProps } from "./Surface.js";
|
|
11
11
|
export { Dialog } from "./Dialog.js";
|
|
12
12
|
export { Progress } from "./Progress.js";
|
|
13
|
+
export { Tabs, type TabsProps, type TabOption } from "./Tabs.js";
|
|
13
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AACvE,OAAO,EACL,QAAQ,EACR,MAAM,EACN,KAAK,qBAAqB,GAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EACL,OAAO,EACP,IAAI,EACJ,KAAK,YAAY,EACjB,KAAK,SAAS,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AACvE,OAAO,EACL,QAAQ,EACR,MAAM,EACN,KAAK,qBAAqB,GAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EACL,OAAO,EACP,IAAI,EACJ,KAAK,YAAY,EACjB,KAAK,SAAS,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/styles.css
CHANGED
|
@@ -33,9 +33,10 @@
|
|
|
33
33
|
border: 1px solid transparent;
|
|
34
34
|
border-radius: var(--lo-radius-full);
|
|
35
35
|
min-width: 0;
|
|
36
|
+
flex-shrink: 0;
|
|
36
37
|
max-width: 100%;
|
|
37
38
|
white-space: normal;
|
|
38
|
-
overflow-wrap:
|
|
39
|
+
overflow-wrap: break-word;
|
|
39
40
|
text-align: center;
|
|
40
41
|
display: inline-flex;
|
|
41
42
|
align-items: center;
|
|
@@ -147,18 +148,76 @@
|
|
|
147
148
|
.lo-ui-field__input {
|
|
148
149
|
width: 100%;
|
|
149
150
|
min-width: 0;
|
|
150
|
-
min-height:
|
|
151
|
-
padding: 10px
|
|
151
|
+
min-height: 44px;
|
|
152
|
+
padding: 9px 10px;
|
|
152
153
|
color: var(--lo-color-text);
|
|
153
154
|
background: var(--lo-color-surface);
|
|
154
155
|
border: 1px solid var(--lo-color-control-border);
|
|
155
|
-
border-radius:
|
|
156
|
+
border-radius: 10px;
|
|
156
157
|
font-size: 16px;
|
|
157
158
|
transition:
|
|
158
159
|
border-color var(--lo-motion-fast) var(--lo-ease-out),
|
|
159
160
|
box-shadow var(--lo-motion-fast) var(--lo-ease-out);
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
.lo-ui-field__input--search {
|
|
164
|
+
min-height: 38px;
|
|
165
|
+
padding: 6px 15px;
|
|
166
|
+
border-color: transparent;
|
|
167
|
+
border-radius: 6px;
|
|
168
|
+
background: #76768015;
|
|
169
|
+
font-size: 14px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.lo-ui-tabs {
|
|
173
|
+
position: relative;
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 4px;
|
|
177
|
+
min-width: 0;
|
|
178
|
+
max-width: 100%;
|
|
179
|
+
overflow-x: auto;
|
|
180
|
+
overscroll-behavior-x: contain;
|
|
181
|
+
scrollbar-width: none;
|
|
182
|
+
padding: 2px;
|
|
183
|
+
}
|
|
184
|
+
.lo-ui-tabs::-webkit-scrollbar {
|
|
185
|
+
display: none;
|
|
186
|
+
}
|
|
187
|
+
.lo-ui-tab {
|
|
188
|
+
position: relative;
|
|
189
|
+
isolation: isolate;
|
|
190
|
+
flex: 0 0 auto;
|
|
191
|
+
min-height: 44px;
|
|
192
|
+
padding: 0 15px;
|
|
193
|
+
border: 0;
|
|
194
|
+
border-radius: var(--lo-radius-full);
|
|
195
|
+
color: var(--lo-color-text-secondary);
|
|
196
|
+
background: transparent;
|
|
197
|
+
font-size: var(--lo-font-size-label);
|
|
198
|
+
font-weight: var(--lo-font-weight-medium);
|
|
199
|
+
white-space: nowrap;
|
|
200
|
+
cursor: pointer;
|
|
201
|
+
}
|
|
202
|
+
.lo-ui-tab[aria-selected="true"] {
|
|
203
|
+
color: var(--lo-color-accent-text);
|
|
204
|
+
}
|
|
205
|
+
.lo-ui-tab[aria-selected="true"]::before {
|
|
206
|
+
content: "";
|
|
207
|
+
position: absolute;
|
|
208
|
+
z-index: -1;
|
|
209
|
+
inset: 5px 0;
|
|
210
|
+
border-radius: var(--lo-radius-full);
|
|
211
|
+
background: var(--lo-color-accent-soft);
|
|
212
|
+
}
|
|
213
|
+
.lo-ui-tab:disabled {
|
|
214
|
+
opacity: 0.5;
|
|
215
|
+
cursor: default;
|
|
216
|
+
}
|
|
217
|
+
.lo-ui-tab:focus-visible {
|
|
218
|
+
outline-offset: -3px;
|
|
219
|
+
}
|
|
220
|
+
|
|
162
221
|
.lo-ui-field__input::placeholder {
|
|
163
222
|
color: var(--lo-color-text-secondary);
|
|
164
223
|
opacity: 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lo-ink/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Accessible React components for LO miniapps",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"prepack": "npm run build",
|
|
33
|
-
"build": "
|
|
33
|
+
"build": "node ../../scripts/build-package.mjs"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@lo-ink/design-tokens": "0.3.
|
|
36
|
+
"@lo-ink/design-tokens": "0.3.1"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"react": ">=18 <20",
|
package/src/Surface.tsx
CHANGED
|
@@ -5,19 +5,15 @@ import { classes } from "./utils.js";
|
|
|
5
5
|
export interface SurfaceProps extends HTMLAttributes<HTMLElement> {
|
|
6
6
|
padding?: SpacingToken;
|
|
7
7
|
}
|
|
8
|
-
export function Surface({
|
|
9
|
-
padding = 4,
|
|
10
|
-
className,
|
|
11
|
-
style,
|
|
12
|
-
...props
|
|
13
|
-
}: SurfaceProps) {
|
|
8
|
+
export function Surface({ padding, className, style, ...props }: SurfaceProps) {
|
|
14
9
|
return (
|
|
15
10
|
<section
|
|
16
11
|
{...props}
|
|
17
12
|
className={classes("lo-ui-surface", className)}
|
|
18
13
|
style={
|
|
19
14
|
{
|
|
20
|
-
"--lo-surface-padding":
|
|
15
|
+
"--lo-surface-padding":
|
|
16
|
+
padding === undefined ? "10px" : `var(--lo-space-${padding})`,
|
|
21
17
|
...style,
|
|
22
18
|
} as CSSProperties
|
|
23
19
|
}
|
package/src/Tabs.tsx
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useId,
|
|
4
|
+
useRef,
|
|
5
|
+
type HTMLAttributes,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { classes } from "./utils.js";
|
|
9
|
+
|
|
10
|
+
export interface TabOption {
|
|
11
|
+
value: string;
|
|
12
|
+
label: ReactNode;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
panelId?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface TabsProps extends Omit<
|
|
17
|
+
HTMLAttributes<HTMLDivElement>,
|
|
18
|
+
"onChange"
|
|
19
|
+
> {
|
|
20
|
+
options: readonly TabOption[];
|
|
21
|
+
value: string;
|
|
22
|
+
onValueChange: (value: string) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Controlled horizontal tabs. Labels retain their width and scroll into view. */
|
|
26
|
+
export function Tabs({
|
|
27
|
+
options,
|
|
28
|
+
value,
|
|
29
|
+
onValueChange,
|
|
30
|
+
id: providedId,
|
|
31
|
+
className,
|
|
32
|
+
...props
|
|
33
|
+
}: TabsProps) {
|
|
34
|
+
const generatedId = useId();
|
|
35
|
+
const id = providedId ?? generatedId;
|
|
36
|
+
const row = useRef<HTMLDivElement>(null);
|
|
37
|
+
const focusValue = options.some(
|
|
38
|
+
(option) => option.value === value && !option.disabled,
|
|
39
|
+
)
|
|
40
|
+
? value
|
|
41
|
+
: options.find((option) => !option.disabled)?.value;
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const selected = row.current?.querySelector<HTMLElement>(
|
|
44
|
+
'[aria-selected="true"]',
|
|
45
|
+
);
|
|
46
|
+
if (!selected || !row.current) return;
|
|
47
|
+
const viewport = row.current;
|
|
48
|
+
const bounds = selected.getBoundingClientRect();
|
|
49
|
+
const visible = viewport.getBoundingClientRect();
|
|
50
|
+
if (bounds.left < visible.left)
|
|
51
|
+
viewport.scrollLeft += bounds.left - visible.left;
|
|
52
|
+
else if (bounds.right > visible.right)
|
|
53
|
+
viewport.scrollLeft += bounds.right - visible.right;
|
|
54
|
+
}, [value, options]);
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
{...props}
|
|
58
|
+
id={id}
|
|
59
|
+
ref={row}
|
|
60
|
+
role="tablist"
|
|
61
|
+
aria-orientation="horizontal"
|
|
62
|
+
className={classes("lo-ui-tabs", className)}
|
|
63
|
+
>
|
|
64
|
+
{options.map((option, index) => (
|
|
65
|
+
<button
|
|
66
|
+
key={option.value}
|
|
67
|
+
id={`${id}-tab-${index}`}
|
|
68
|
+
type="button"
|
|
69
|
+
role="tab"
|
|
70
|
+
aria-selected={option.value === value}
|
|
71
|
+
aria-controls={option.panelId}
|
|
72
|
+
tabIndex={option.value === focusValue ? 0 : -1}
|
|
73
|
+
disabled={option.disabled}
|
|
74
|
+
className="lo-ui-tab"
|
|
75
|
+
onClick={() => onValueChange(option.value)}
|
|
76
|
+
onKeyDown={(event) => {
|
|
77
|
+
if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key))
|
|
78
|
+
return;
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
const enabled = options
|
|
81
|
+
.map((item, i) => (item.disabled ? -1 : i))
|
|
82
|
+
.filter((i) => i >= 0);
|
|
83
|
+
if (!enabled.length) return;
|
|
84
|
+
const current = enabled.indexOf(index);
|
|
85
|
+
const forward =
|
|
86
|
+
getComputedStyle(event.currentTarget).direction === "rtl"
|
|
87
|
+
? "ArrowLeft"
|
|
88
|
+
: "ArrowRight";
|
|
89
|
+
const next =
|
|
90
|
+
event.key === "Home"
|
|
91
|
+
? enabled[0]
|
|
92
|
+
: event.key === "End"
|
|
93
|
+
? enabled[enabled.length - 1]
|
|
94
|
+
: enabled[
|
|
95
|
+
(current +
|
|
96
|
+
(event.key === forward ? 1 : -1) +
|
|
97
|
+
enabled.length) %
|
|
98
|
+
enabled.length
|
|
99
|
+
];
|
|
100
|
+
onValueChange(options[next].value);
|
|
101
|
+
const buttons =
|
|
102
|
+
row.current?.querySelectorAll<HTMLButtonElement>('[role="tab"]');
|
|
103
|
+
buttons?.[next]?.focus();
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
{option.label}
|
|
107
|
+
</button>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
package/src/TextField.tsx
CHANGED
|
@@ -15,6 +15,7 @@ export interface TextFieldProps extends Omit<
|
|
|
15
15
|
error?: ReactNode;
|
|
16
16
|
inputClassName?: string;
|
|
17
17
|
labelHidden?: boolean;
|
|
18
|
+
variant?: "outlined" | "search";
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
@@ -27,6 +28,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
27
28
|
className,
|
|
28
29
|
inputClassName,
|
|
29
30
|
labelHidden = false,
|
|
31
|
+
variant = "outlined",
|
|
30
32
|
"aria-describedby": ariaDescribedBy,
|
|
31
33
|
...props
|
|
32
34
|
},
|
|
@@ -44,6 +46,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
44
46
|
<div
|
|
45
47
|
className={classes(
|
|
46
48
|
"lo-ui-field",
|
|
49
|
+
`lo-ui-field--${variant}`,
|
|
47
50
|
labelHidden && "lo-ui-field--label-hidden",
|
|
48
51
|
className,
|
|
49
52
|
)}
|
|
@@ -57,7 +60,11 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|
|
57
60
|
id={id}
|
|
58
61
|
aria-invalid={error ? true : props["aria-invalid"]}
|
|
59
62
|
aria-describedby={describedBy || undefined}
|
|
60
|
-
className={classes(
|
|
63
|
+
className={classes(
|
|
64
|
+
"lo-ui-field__input",
|
|
65
|
+
variant === "search" && "lo-ui-field__input--search",
|
|
66
|
+
inputClassName,
|
|
67
|
+
)}
|
|
61
68
|
/>
|
|
62
69
|
{description && (
|
|
63
70
|
<div className="lo-ui-field__description" id={descriptionId}>
|
package/src/index.ts
CHANGED
|
@@ -19,3 +19,4 @@ export { TextArea, type TextAreaProps } from "./TextArea.js";
|
|
|
19
19
|
export { Surface, type SurfaceProps } from "./Surface.js";
|
|
20
20
|
export { Dialog } from "./Dialog.js";
|
|
21
21
|
export { Progress } from "./Progress.js";
|
|
22
|
+
export { Tabs, type TabsProps, type TabOption } from "./Tabs.js";
|
package/src/styles.css
CHANGED
|
@@ -33,9 +33,10 @@
|
|
|
33
33
|
border: 1px solid transparent;
|
|
34
34
|
border-radius: var(--lo-radius-full);
|
|
35
35
|
min-width: 0;
|
|
36
|
+
flex-shrink: 0;
|
|
36
37
|
max-width: 100%;
|
|
37
38
|
white-space: normal;
|
|
38
|
-
overflow-wrap:
|
|
39
|
+
overflow-wrap: break-word;
|
|
39
40
|
text-align: center;
|
|
40
41
|
display: inline-flex;
|
|
41
42
|
align-items: center;
|
|
@@ -147,18 +148,76 @@
|
|
|
147
148
|
.lo-ui-field__input {
|
|
148
149
|
width: 100%;
|
|
149
150
|
min-width: 0;
|
|
150
|
-
min-height:
|
|
151
|
-
padding: 10px
|
|
151
|
+
min-height: 44px;
|
|
152
|
+
padding: 9px 10px;
|
|
152
153
|
color: var(--lo-color-text);
|
|
153
154
|
background: var(--lo-color-surface);
|
|
154
155
|
border: 1px solid var(--lo-color-control-border);
|
|
155
|
-
border-radius:
|
|
156
|
+
border-radius: 10px;
|
|
156
157
|
font-size: 16px;
|
|
157
158
|
transition:
|
|
158
159
|
border-color var(--lo-motion-fast) var(--lo-ease-out),
|
|
159
160
|
box-shadow var(--lo-motion-fast) var(--lo-ease-out);
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
.lo-ui-field__input--search {
|
|
164
|
+
min-height: 38px;
|
|
165
|
+
padding: 6px 15px;
|
|
166
|
+
border-color: transparent;
|
|
167
|
+
border-radius: 6px;
|
|
168
|
+
background: #76768015;
|
|
169
|
+
font-size: 14px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.lo-ui-tabs {
|
|
173
|
+
position: relative;
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 4px;
|
|
177
|
+
min-width: 0;
|
|
178
|
+
max-width: 100%;
|
|
179
|
+
overflow-x: auto;
|
|
180
|
+
overscroll-behavior-x: contain;
|
|
181
|
+
scrollbar-width: none;
|
|
182
|
+
padding: 2px;
|
|
183
|
+
}
|
|
184
|
+
.lo-ui-tabs::-webkit-scrollbar {
|
|
185
|
+
display: none;
|
|
186
|
+
}
|
|
187
|
+
.lo-ui-tab {
|
|
188
|
+
position: relative;
|
|
189
|
+
isolation: isolate;
|
|
190
|
+
flex: 0 0 auto;
|
|
191
|
+
min-height: 44px;
|
|
192
|
+
padding: 0 15px;
|
|
193
|
+
border: 0;
|
|
194
|
+
border-radius: var(--lo-radius-full);
|
|
195
|
+
color: var(--lo-color-text-secondary);
|
|
196
|
+
background: transparent;
|
|
197
|
+
font-size: var(--lo-font-size-label);
|
|
198
|
+
font-weight: var(--lo-font-weight-medium);
|
|
199
|
+
white-space: nowrap;
|
|
200
|
+
cursor: pointer;
|
|
201
|
+
}
|
|
202
|
+
.lo-ui-tab[aria-selected="true"] {
|
|
203
|
+
color: var(--lo-color-accent-text);
|
|
204
|
+
}
|
|
205
|
+
.lo-ui-tab[aria-selected="true"]::before {
|
|
206
|
+
content: "";
|
|
207
|
+
position: absolute;
|
|
208
|
+
z-index: -1;
|
|
209
|
+
inset: 5px 0;
|
|
210
|
+
border-radius: var(--lo-radius-full);
|
|
211
|
+
background: var(--lo-color-accent-soft);
|
|
212
|
+
}
|
|
213
|
+
.lo-ui-tab:disabled {
|
|
214
|
+
opacity: 0.5;
|
|
215
|
+
cursor: default;
|
|
216
|
+
}
|
|
217
|
+
.lo-ui-tab:focus-visible {
|
|
218
|
+
outline-offset: -3px;
|
|
219
|
+
}
|
|
220
|
+
|
|
162
221
|
.lo-ui-field__input::placeholder {
|
|
163
222
|
color: var(--lo-color-text-secondary);
|
|
164
223
|
opacity: 1;
|