@lo-ink/ui 0.3.1 → 0.4.1

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.
@@ -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,EACtB,OAAW,EACX,SAAS,EACT,KAAK,EACL,GAAG,KAAK,EACT,EAAE,YAAY,+BAad"}
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 = 4, className, style, ...props }) {
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
+ }
@@ -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
@@ -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;CACvB;AAED,eAAO,MAAM,SAAS,6GAuDrB,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
@@ -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
@@ -10,3 +10,4 @@ export { TextArea } from "./TextArea.js";
10
10
  export { Surface } from "./Surface.js";
11
11
  export { Dialog } from "./Dialog.js";
12
12
  export { Progress } from "./Progress.js";
13
+ export { Tabs } from "./Tabs.js";
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: anywhere;
39
+ overflow-wrap: break-word;
39
40
  text-align: center;
40
41
  display: inline-flex;
41
42
  align-items: center;
@@ -147,18 +148,75 @@
147
148
  .lo-ui-field__input {
148
149
  width: 100%;
149
150
  min-width: 0;
150
- min-height: var(--lo-size-control);
151
- padding: 10px var(--lo-space-3);
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: var(--lo-radius-md);
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
+ }
170
+
171
+ .lo-ui-tabs {
172
+ position: relative;
173
+ display: flex;
174
+ align-items: center;
175
+ gap: 4px;
176
+ min-width: 0;
177
+ max-width: 100%;
178
+ overflow-x: auto;
179
+ overscroll-behavior-x: contain;
180
+ scrollbar-width: none;
181
+ padding: 2px;
182
+ }
183
+ .lo-ui-tabs::-webkit-scrollbar {
184
+ display: none;
185
+ }
186
+ .lo-ui-tab {
187
+ position: relative;
188
+ isolation: isolate;
189
+ flex: 0 0 auto;
190
+ min-height: 44px;
191
+ padding: 0 15px;
192
+ border: 0;
193
+ border-radius: var(--lo-radius-full);
194
+ color: var(--lo-color-text-secondary);
195
+ background: transparent;
196
+ font-size: var(--lo-font-size-label);
197
+ font-weight: var(--lo-font-weight-medium);
198
+ white-space: nowrap;
199
+ cursor: pointer;
200
+ }
201
+ .lo-ui-tab[aria-selected="true"] {
202
+ color: var(--lo-color-accent-text);
203
+ }
204
+ .lo-ui-tab[aria-selected="true"]::before {
205
+ content: "";
206
+ position: absolute;
207
+ z-index: -1;
208
+ inset: 5px 0;
209
+ border-radius: var(--lo-radius-full);
210
+ background: var(--lo-color-accent-soft);
211
+ }
212
+ .lo-ui-tab:disabled {
213
+ opacity: 0.5;
214
+ cursor: default;
215
+ }
216
+ .lo-ui-tab:focus-visible {
217
+ outline-offset: -3px;
218
+ }
219
+
162
220
  .lo-ui-field__input::placeholder {
163
221
  color: var(--lo-color-text-secondary);
164
222
  opacity: 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lo-ink/ui",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Accessible React components for LO miniapps",
5
5
  "type": "module",
6
6
  "license": "MIT",
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": `var(--lo-space-${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("lo-ui-field__input", inputClassName)}
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: anywhere;
39
+ overflow-wrap: break-word;
39
40
  text-align: center;
40
41
  display: inline-flex;
41
42
  align-items: center;
@@ -147,18 +148,75 @@
147
148
  .lo-ui-field__input {
148
149
  width: 100%;
149
150
  min-width: 0;
150
- min-height: var(--lo-size-control);
151
- padding: 10px var(--lo-space-3);
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: var(--lo-radius-md);
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
+ }
170
+
171
+ .lo-ui-tabs {
172
+ position: relative;
173
+ display: flex;
174
+ align-items: center;
175
+ gap: 4px;
176
+ min-width: 0;
177
+ max-width: 100%;
178
+ overflow-x: auto;
179
+ overscroll-behavior-x: contain;
180
+ scrollbar-width: none;
181
+ padding: 2px;
182
+ }
183
+ .lo-ui-tabs::-webkit-scrollbar {
184
+ display: none;
185
+ }
186
+ .lo-ui-tab {
187
+ position: relative;
188
+ isolation: isolate;
189
+ flex: 0 0 auto;
190
+ min-height: 44px;
191
+ padding: 0 15px;
192
+ border: 0;
193
+ border-radius: var(--lo-radius-full);
194
+ color: var(--lo-color-text-secondary);
195
+ background: transparent;
196
+ font-size: var(--lo-font-size-label);
197
+ font-weight: var(--lo-font-weight-medium);
198
+ white-space: nowrap;
199
+ cursor: pointer;
200
+ }
201
+ .lo-ui-tab[aria-selected="true"] {
202
+ color: var(--lo-color-accent-text);
203
+ }
204
+ .lo-ui-tab[aria-selected="true"]::before {
205
+ content: "";
206
+ position: absolute;
207
+ z-index: -1;
208
+ inset: 5px 0;
209
+ border-radius: var(--lo-radius-full);
210
+ background: var(--lo-color-accent-soft);
211
+ }
212
+ .lo-ui-tab:disabled {
213
+ opacity: 0.5;
214
+ cursor: default;
215
+ }
216
+ .lo-ui-tab:focus-visible {
217
+ outline-offset: -3px;
218
+ }
219
+
162
220
  .lo-ui-field__input::placeholder {
163
221
  color: var(--lo-color-text-secondary);
164
222
  opacity: 1;