addon-ui 0.9.2 → 0.10.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
@@ -67,12 +67,12 @@ This library now ships with dedicated documentation files for each component in
67
67
  - [IconButton](docs/IconButton.md)
68
68
  - [List](docs/List.md) (covers List and ListItem)
69
69
  - [Modal](docs/Modal.md)
70
- - [Odometer](docs/Odometer.md) (component + useOdometer hook)
70
+ - [Odometer](docs/Odometer.md) (component + `useOdometer` hook)
71
71
  - [ScrollArea](docs/ScrollArea.md)
72
72
  - [Select](docs/Select.md)
73
73
  - [SvgSprite](docs/SvgSprite.md)
74
74
  - [Switch](docs/Switch.md)
75
- - [Tabs](docs/Tabs.md) (includes Tabs, TabsList, TabsTrigger, TabsContent)
75
+ - [Tabs](docs/Tabs.md) (includes `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`)
76
76
  - [Tag](docs/Tag.md)
77
77
  - [TextArea](docs/TextArea.md)
78
78
  - [TextField](docs/TextField.md)
@@ -83,7 +83,7 @@ This library now ships with dedicated documentation files for each component in
83
83
  - [View](docs/View.md)
84
84
  - [ViewDrawer](docs/ViewDrawer.md)
85
85
  - [ViewModal](docs/ViewModal.md)
86
- - [Viewport](docs/Viewport.md) (ViewportProvider + useViewport)
86
+ - [Viewport](docs/Viewport.md) (`ViewportProvider` + `useViewport`)
87
87
 
88
88
  Notes:
89
89
 
@@ -134,15 +134,40 @@ export default defineConfig({
134
134
  plugins: [
135
135
  ui({
136
136
  themeDir: "./theme", // Directory for theme files
137
- configFileName: "ui.config", // Name of config files
138
- styleFileName: "ui.style", // Name of style files
137
+ configName: "ui.config", // Name of config files
138
+ styleName: "ui.style", // Name of style files
139
139
  mergeConfig: true, // Merge configs from different directories
140
140
  mergeStyles: true, // Merge styles from different directories
141
+ splitChunks: true, // Enable automatic chunk splitting for components
141
142
  }),
142
143
  ],
143
144
  });
144
145
  ```
145
146
 
147
+ ### Plugin Options
148
+
149
+ | Option | Type | Default | Description |
150
+ | :------------ | :------------------------------------------------- | :------------ | :----------------------------------------------------------------------------------------------------- |
151
+ | `themeDir` | `string` | `"."` | Directory path where plugin configuration and style files are located. |
152
+ | `configName` | `string` | `"config.ui"` | Name of the configuration file. |
153
+ | `styleName` | `string` | `"style.ui"` | Name of the SCSS style file. |
154
+ | `mergeConfig` | `boolean` | `true` | Whether to merge configuration files from different directories. |
155
+ | `mergeStyles` | `boolean` | `true` | Whether to merge style files from different directories. |
156
+ | `splitChunks` | `boolean \| (name: string) => string \| undefined` | `true` | Enables automatic chunk splitting. If a function is provided, it can be used to customize chunk names. |
157
+
158
+ #### Customizing Chunk Names
159
+
160
+ You can pass a callback function to `splitChunks` to customize the generated chunk names:
161
+
162
+ ```ts
163
+ ui({
164
+ splitChunks: name => {
165
+ if (name === "button") return "ui-core-button";
166
+ return `ui-${name}`;
167
+ },
168
+ });
169
+ ```
170
+
146
171
  ### Configuration Files
147
172
 
148
173
  The `addon-ui` configuration is designed to retrieve configuration from each extension separately, allowing for
@@ -271,7 +296,7 @@ extensions with the same functionality but different visual appearances.
271
296
 
272
297
  ### Global Theme Customization
273
298
 
274
- You can customize the theme globally by passing props to the UIProvider:
299
+ You can customize the theme globally by passing props to the `UIProvider`:
275
300
 
276
301
  ```jsx
277
302
  import {UIProvider} from "addon-ui";
@@ -289,6 +314,8 @@ const customTheme = {
289
314
  icons: {
290
315
  // Custom icons
291
316
  },
317
+ // Specify the DOM element to set theme/view/browser attributes on
318
+ container: "#app-root",
292
319
  };
293
320
 
294
321
  function App() {
@@ -296,6 +323,17 @@ function App() {
296
323
  }
297
324
  ```
298
325
 
326
+ ### UIProvider Props
327
+
328
+ | Prop | Type | Default | Description |
329
+ | :----------- | :----------------------------- | :---------- | :-------------------------------------------------------- |
330
+ | `components` | `ComponentsProps` | `{}` | Component-specific configuration overrides. |
331
+ | `icons` | `Icons` | `{}` | Custom SVG icons registration. |
332
+ | `extra` | `ExtraProps` | `{}` | App-wide extra properties. |
333
+ | `storage` | `ThemeStorageContract \| true` | `undefined` | Persistence storage for theme settings. |
334
+ | `container` | `string \| Element \| false` | `"html"` | Target element for attributes. Set to `false` to disable. |
335
+ | `view` | `string` | `undefined` | Custom view identifier for specific styling. |
336
+
299
337
  ### Using Extra Props
300
338
 
301
339
  Extra Props is a powerful feature that allows you to extend component props with custom properties. This is particularly
@@ -438,12 +476,13 @@ function App() {
438
476
 
439
477
  ## Theming and style reuse
440
478
 
441
- - Global theme tokens (colors, typography, spacing, transitions) live in your ui.style.scss. Components consume them
442
- through fallbacks.
443
- - Each component also exposes its own `--component-*` variables. See the CSS variables tables in the docs to know
444
- exactly what you can override.
445
- - Light/dark modes: use `@import "addon-ui/theme";` and the provided `@include light { ... }` / `@include dark { ... }`
446
- mixins in your theme SCSS to scope tokens per color scheme.
479
+ - Global theme tokens (colors, typography, spacing, transitions) live in your `ui.style.scss`.
480
+ - Each component also exposes its own `--component-*` variables. See the CSS variables tables in the docs to know exactly what you can override.
481
+ - **Theme Mixins**: Use `@import "addon-ui/theme";` to access `@include light { ... }` and `@include dark { ... }` mixins.
482
+ - **Universal Targeting**: These mixins are container-agnostic. They work correctly whether the `theme` attribute is on a parent element or directly on the component itself.
483
+ - **Context-Aware**:
484
+ - When used at the top level, they generate global selectors: `[theme="dark"] { ... }`.
485
+ - When used inside a component, they generate scoped selectors: `[theme="dark"] .my-comp, .my-comp[theme="dark"] { ... }`.
447
486
 
448
487
  ## Radix UI and third-party integrations
449
488
 
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { SelectProps } from "@radix-ui/react-select";
3
- export { SelectProps };
3
+ export { type SelectProps };
4
4
  declare const _default: React.NamedExoticComponent<SelectProps>;
5
5
  export default _default;
6
6
  //# sourceMappingURL=Select.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { SelectIconProps } from "@radix-ui/react-select";
3
- export { SelectIconProps };
3
+ export { type SelectIconProps };
4
4
  declare const _default: React.NamedExoticComponent<SelectIconProps & React.RefAttributes<HTMLSpanElement>>;
5
5
  export default _default;
6
6
  //# sourceMappingURL=SelectIcon.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { SelectValueProps } from "@radix-ui/react-select";
3
- export { SelectValueProps };
3
+ export { type SelectValueProps };
4
4
  declare const _default: React.NamedExoticComponent<SelectValueProps & React.RefAttributes<HTMLSpanElement>>;
5
5
  export default _default;
6
6
  //# sourceMappingURL=SelectValue.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import React, { ComponentProps, ReactNode } from "react";
2
- import { TextFieldVariant, TextFieldSize, TextFieldRadius, TextFieldAccent } from "./types";
2
+ import { TextFieldAccent, TextFieldRadius, TextFieldSize, TextFieldVariant } from "./types";
3
3
  export interface TextFieldActions {
4
4
  select(): void;
5
5
  focus(): void;
@@ -20,6 +20,7 @@ export interface TextFieldProps extends ComponentProps<"input"> {
20
20
  inputClassName?: string;
21
21
  afterClassName?: string;
22
22
  beforeClassName?: string;
23
+ strict?: boolean;
23
24
  }
24
25
  declare const _default: React.NamedExoticComponent<Omit<TextFieldProps, "ref"> & React.RefAttributes<TextFieldActions>>;
25
26
  export default _default;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Normalizes user-typed numeric input.
3
+ *
4
+ * - Allows partial values ("-", ".", "1e", "1e-")
5
+ * - Supports decimals and scientific notation
6
+ */
7
+ export declare const normalizeNumberInput: (raw: string) => string;
8
+ //# sourceMappingURL=utils.d.ts.map
@@ -1,10 +1,10 @@
1
1
  import React, { ComponentProps } from "react";
2
- import { HighlightProps } from "../Highlight";
3
2
  export interface TruncateProps extends ComponentProps<"span"> {
4
3
  text?: string;
5
4
  middle?: boolean;
6
5
  separator?: string;
7
- highlight?: Omit<HighlightProps, "textToHighlight">;
6
+ contentClassname?: string;
7
+ render?: (text: string) => React.ReactNode;
8
8
  }
9
9
  declare const _default: React.NamedExoticComponent<Omit<TruncateProps, "ref"> & React.RefAttributes<HTMLSpanElement>>;
10
10
  export default _default;
@@ -0,0 +1,2 @@
1
+ export declare const calculateMiddleTruncate: (text: string, maxWidth: number, font: string, letterSpacing: string, separator: string) => string;
2
+ //# sourceMappingURL=utils.d.ts.map
@@ -1,9 +1,35 @@
1
1
  export interface PluginOptions {
2
+ /**
3
+ * Directory path where plugin configuration and style files are located, relative to the project root.
4
+ * @default "."
5
+ */
2
6
  themeDir?: string;
3
- configFileName?: string;
4
- styleFileName?: string;
7
+ /**
8
+ * Name of the configuration file.
9
+ * @default "config.ui"
10
+ */
11
+ configName?: string;
12
+ /**
13
+ * Name of the style file.
14
+ * @default "style.ui"
15
+ */
16
+ styleName?: string;
17
+ /**
18
+ * Whether to merge configuration files from different app directories.
19
+ * @default true
20
+ */
5
21
  mergeConfig?: boolean;
22
+ /**
23
+ * Whether to merge style files from different app directories.
24
+ * @default true
25
+ */
6
26
  mergeStyles?: boolean;
27
+ /**
28
+ * Configuration for splitting chunks.
29
+ * Can be a boolean to enable/disable or a callback to customize chunk names.
30
+ * @default true
31
+ */
32
+ splitChunks?: boolean | ((name: string) => string | undefined);
7
33
  }
8
34
  declare const _default: import("adnbn").PluginDefinition<[options?: PluginOptions | undefined]>;
9
35
  export default _default;
@@ -2,7 +2,95 @@ import { FC, PropsWithChildren } from "react";
2
2
  import { ThemeStorageContract } from "../../types/theme";
3
3
  import { Config } from "../../types/config";
4
4
  export interface ThemeProviderProps extends Pick<Config, "components"> {
5
+ /**
6
+ * Theme persistence storage configuration.
7
+ *
8
+ * @remarks
9
+ * - When `undefined`, theme changes are stored only in component state (memory) and reset on page reload.
10
+ * - When `true`, uses the default `ThemeStorage` implementation (typically localStorage).
11
+ * - When a custom `ThemeStorageContract` object is provided, uses that implementation for theme persistence.
12
+ *
13
+ * The storage is used to save, retrieve, and watch for theme changes across sessions or tabs.
14
+ *
15
+ * @default undefined
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * // No persistence (memory only)
20
+ * <ThemeProvider>
21
+ * <App />
22
+ * </ThemeProvider>
23
+ * ```
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * // Use default storage (localStorage)
28
+ * <ThemeProvider storage={true}>
29
+ * <App />
30
+ * </ThemeProvider>
31
+ * ```
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * // Use custom storage implementation
36
+ * const customStorage: ThemeStorageContract = {
37
+ * get: async () => { ... },
38
+ * change: async (theme) => { ... },
39
+ * toggle: async () => { ... },
40
+ * watch: (callback) => { ... }
41
+ * };
42
+ *
43
+ * <ThemeProvider storage={customStorage}>
44
+ * <App />
45
+ * </ThemeProvider>
46
+ * ```
47
+ */
5
48
  storage?: ThemeStorageContract | true;
49
+ /**
50
+ * The DOM element where the provider will set attributes "browser"
51
+ *
52
+ * @remarks
53
+ * - When a string is provided, it's used as a CSS selector to find the element via `document.querySelector`.
54
+ * - When an Element is provided, attributes are set directly on that element.
55
+ * - When `false`, no element attributes are set.
56
+ *
57
+ * Attributes are automatically cleaned up when the component unmounts.
58
+ *
59
+ * @default "html"
60
+ *
61
+ * @example
62
+ * ```tsx
63
+ * // Use default html element
64
+ * <ThemeProviderProps>
65
+ * <App />
66
+ * </ThemeProviderProps>
67
+ * ```
68
+ *
69
+ * @example
70
+ * ```tsx
71
+ * // Use custom selector
72
+ * <ThemeProviderProps container="#app-root">
73
+ * <App />
74
+ * </ThemeProviderProps>
75
+ * ```
76
+ *
77
+ * @example
78
+ * ```tsx
79
+ * // Use direct element reference
80
+ * <ThemeProviderProps container={document.body}>
81
+ * <App />
82
+ * </ThemeProviderProps>
83
+ * ```
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * // Disable container attributes
88
+ * <ThemeProviderProps container={false}>
89
+ * <App />
90
+ * </ThemeProviderProps>
91
+ * ```
92
+ */
93
+ container?: string | Element | false;
6
94
  }
7
95
  declare const ThemeProvider: FC<PropsWithChildren<ThemeProviderProps>>;
8
96
  export default ThemeProvider;
@@ -4,7 +4,28 @@ import { Config } from "../../types/config";
4
4
  import "./styles/default.scss";
5
5
  import "./styles/reset.scss";
6
6
  import "addon-ui-style.scss";
7
- export interface UIProviderProps extends Partial<Config>, Pick<ThemeProviderProps, "storage"> {
7
+ export interface UIProviderProps extends Partial<Config>, Pick<ThemeProviderProps, "storage" | "container"> {
8
+ /**
9
+ * A custom view identifier that allows developers to specify a unique name for styling customization.
10
+ * This value is set as a "view" attribute on the container element and can be targeted through SCSS mixins
11
+ * to apply view-specific styles and behavior.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <UIProvider view="dashboard">
16
+ * <App />
17
+ * </UIProvider>
18
+ * ```
19
+ *
20
+ * @example
21
+ * ```scss
22
+ * @include view("dashboard") {
23
+ * .some-class {
24
+ * // Custom styles for dashboard view
25
+ * }
26
+ * }
27
+ * ```
28
+ */
8
29
  view?: string;
9
30
  }
10
31
  declare const UIProvider: FC<PropsWithChildren<UIProviderProps>>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "addon-ui",
3
3
  "type": "module",
4
- "version": "0.9.2",
4
+ "version": "0.10.0",
5
5
  "description": "A comprehensive React UI component library designed exclusively for the AddonBone browser extension framework with customizable theming and consistent design patterns",
6
6
  "keywords": [
7
7
  "react",
@@ -106,7 +106,7 @@
106
106
  "@types/node": "^22.13.10",
107
107
  "@types/react": "^19.0.10",
108
108
  "@types/react-dom": "^19.0.4",
109
- "adnbn": "^0.5.4",
109
+ "adnbn": "^0.5.7",
110
110
  "depcheck": "^1.4.7",
111
111
  "eslint": "^9.21.0",
112
112
  "eslint-plugin-react-hooks": "^5.1.0",
@@ -141,12 +141,7 @@
141
141
  },
142
142
  "overrides": {
143
143
  "flat-cache": "^6.1.18",
144
- "html-rspack-tags-plugin": {
145
- "glob": "^10.4.5"
146
- },
147
- "test-exclude": {
148
- "glob": "^10.4.5"
149
- }
144
+ "glob": "^13.0.0"
150
145
  },
151
146
  "eslintConfig": {
152
147
  "extends": [
@@ -6,7 +6,7 @@ import {Root, SelectProps} from "@radix-ui/react-select";
6
6
 
7
7
  import {useComponentProps} from "../../providers";
8
8
 
9
- export {SelectProps};
9
+ export {type SelectProps};
10
10
 
11
11
  const Select: FC<SelectProps> = props => {
12
12
  const {...other} = {...useComponentProps("select"), ...props};
@@ -8,7 +8,7 @@ import {useComponentProps} from "../../providers";
8
8
 
9
9
  import styles from "./select.module.scss";
10
10
 
11
- export {SelectIconProps};
11
+ export {type SelectIconProps};
12
12
 
13
13
  const SelectIcon: ForwardRefRenderFunction<HTMLSpanElement, SelectIconProps> = (props, ref) => {
14
14
  const {className, ...other} = {...useComponentProps("selectIcon"), ...props};
@@ -8,7 +8,7 @@ import {useComponentProps} from "../../providers";
8
8
 
9
9
  import styles from "./select.module.scss";
10
10
 
11
- export {SelectValueProps};
11
+ export {type SelectValueProps};
12
12
 
13
13
  const SelectValue: ForwardRefRenderFunction<HTMLSpanElement, SelectValueProps> = (props, ref) => {
14
14
  const {className, ...other} = {...useComponentProps("selectValue"), ...props};
@@ -1,12 +1,14 @@
1
1
  import React, {
2
- ChangeEventHandler,
2
+ ChangeEvent,
3
3
  ComponentProps,
4
4
  forwardRef,
5
+ KeyboardEvent,
5
6
  memo,
6
7
  ReactNode,
7
8
  useCallback,
8
9
  useEffect,
9
10
  useImperativeHandle,
11
+ useMemo,
10
12
  useRef,
11
13
  useState,
12
14
  } from "react";
@@ -16,7 +18,8 @@ import classnames from "classnames";
16
18
  import {cloneOrCreateElement} from "../../utils";
17
19
  import {useComponentProps} from "../../providers";
18
20
 
19
- import {TextFieldVariant, TextFieldSize, TextFieldRadius, TextFieldAccent} from "./types";
21
+ import {normalizeNumberInput} from "./utils";
22
+ import {TextFieldAccent, TextFieldRadius, TextFieldSize, TextFieldVariant} from "./types";
20
23
 
21
24
  import styles from "./text-field.module.scss";
22
25
 
@@ -44,6 +47,7 @@ export interface TextFieldProps extends ComponentProps<"input"> {
44
47
  inputClassName?: string;
45
48
  afterClassName?: string;
46
49
  beforeClassName?: string;
50
+ strict?: boolean;
47
51
  }
48
52
 
49
53
  const TextField = forwardRef<TextFieldActions, TextFieldProps>((props, ref) => {
@@ -55,7 +59,8 @@ const TextField = forwardRef<TextFieldActions, TextFieldProps>((props, ref) => {
55
59
  label,
56
60
  fullWidth,
57
61
  type = "text",
58
- value: propValue = "",
62
+ strict,
63
+ value: propValue,
59
64
  defaultValue,
60
65
  before,
61
66
  after,
@@ -64,12 +69,20 @@ const TextField = forwardRef<TextFieldActions, TextFieldProps>((props, ref) => {
64
69
  afterClassName,
65
70
  beforeClassName,
66
71
  onChange,
72
+ onKeyDown,
67
73
  ...other
68
74
  } = {...useComponentProps("textField"), ...props};
69
75
 
70
- const [value, setValue] = useState<string | number | undefined>(defaultValue || propValue);
76
+ const [value, setValue] = useState<string>(() => {
77
+ if (propValue != null) return String(propValue);
78
+ if (defaultValue != null) return String(defaultValue);
79
+ return "";
80
+ });
81
+
71
82
  const inputRef = useRef<HTMLInputElement | null>(null);
72
83
 
84
+ const strictNumberType = useMemo(() => type === "number" && !!strict, [type, strict]);
85
+
73
86
  useImperativeHandle(
74
87
  ref,
75
88
  () => ({
@@ -83,22 +96,61 @@ const TextField = forwardRef<TextFieldActions, TextFieldProps>((props, ref) => {
83
96
  return inputRef.current?.value;
84
97
  },
85
98
  setValue(value: string | number | undefined) {
86
- setValue(value);
99
+ setValue(value == null ? "" : String(value));
87
100
  },
88
101
  }),
89
102
  []
90
103
  );
91
104
 
92
- const handleChange = useCallback<ChangeEventHandler<HTMLInputElement>>(
93
- event => {
94
- setValue(event.currentTarget.value);
95
- onChange?.(event);
105
+ const handleChange = useCallback(
106
+ (event: ChangeEvent<HTMLInputElement>) => {
107
+ let newValue = event.currentTarget.value ?? "";
108
+
109
+ if (strictNumberType) {
110
+ newValue = normalizeNumberInput(newValue);
111
+ }
112
+
113
+ setValue(newValue);
114
+
115
+ onChange?.({
116
+ ...event,
117
+ currentTarget: {
118
+ ...event.currentTarget,
119
+ value: newValue,
120
+ },
121
+ });
122
+ },
123
+ [onChange, strictNumberType]
124
+ );
125
+
126
+ const handleKeyDown = useCallback(
127
+ (event: KeyboardEvent<HTMLInputElement>) => {
128
+ if (strictNumberType && event.key.length === 1) {
129
+ // Only handle single-character printable keys here
130
+ // composition and paste handled in onChange
131
+ const {selectionStart, selectionEnd, value} = event.currentTarget;
132
+
133
+ const start = selectionStart ?? value.length;
134
+ const end = selectionEnd ?? start;
135
+
136
+ const next = value.slice(0, start) + event.key + value.slice(end);
137
+ const normalized = normalizeNumberInput(next);
138
+
139
+ if (normalized !== next) {
140
+ event.preventDefault();
141
+ }
142
+ }
143
+
144
+ onKeyDown?.(event);
96
145
  },
97
- [onChange]
146
+ [onKeyDown, strictNumberType]
98
147
  );
99
148
 
100
149
  useEffect(() => {
101
- setValue(propValue);
150
+ const text = propValue == null ? "" : String(propValue);
151
+
152
+ setValue(strictNumberType ? normalizeNumberInput(text) : text);
153
+ // eslint-disable-next-line react-hooks/exhaustive-deps
102
154
  }, [propValue]);
103
155
 
104
156
  return (
@@ -124,12 +176,13 @@ const TextField = forwardRef<TextFieldActions, TextFieldProps>((props, ref) => {
124
176
  <input
125
177
  {...other}
126
178
  ref={inputRef}
127
- type={type}
179
+ type={strictNumberType ? "text" : type}
180
+ inputMode={strictNumberType ? "decimal" : other.inputMode}
128
181
  value={value}
129
- defaultValue={defaultValue}
130
182
  aria-label={label}
131
183
  className={classnames(styles["text-field__input"], inputClassName)}
132
184
  onChange={handleChange}
185
+ onKeyDown={handleKeyDown}
133
186
  />
134
187
  {cloneOrCreateElement(after, {className: classnames(styles["text-field__after"], afterClassName)}, "span")}
135
188
  </div>
@@ -10,7 +10,7 @@ $root: text-field;
10
10
  font-weight: var(--text-field-font-weight, 400);
11
11
  font-size: var(--text-field-font-size, 14px);
12
12
  letter-spacing: var(--text-field-letter-spacing, 0.5px);
13
- line-height: var(--text-field-line-height, var(--line-height, 1 rem));
13
+ line-height: var(--text-field-line-height, var(--line-height, 1rem));
14
14
  padding: var(--text-field-padding, 8px 12px);
15
15
  border-radius: var(--text-field-border-radius, 8px);
16
16
  transition:
@@ -43,10 +43,12 @@ $root: text-field;
43
43
  outline: none;
44
44
  background: transparent;
45
45
  transition: color var(--text-field-speed-color, var(--speed-color));
46
+ appearance: textfield;
46
47
 
47
48
  &:focus {
48
49
  outline: none;
49
50
  }
51
+
50
52
  &:disabled {
51
53
  cursor: not-allowed;
52
54
  }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Normalizes user-typed numeric input.
3
+ *
4
+ * - Allows partial values ("-", ".", "1e", "1e-")
5
+ * - Supports decimals and scientific notation
6
+ */
7
+ export const normalizeNumberInput = (raw: string): string => {
8
+ if (!raw) return "";
9
+
10
+ const filtered = raw.replace(/[^0-9eE+\-.]/g, "");
11
+
12
+ let result = "";
13
+ let hasExponent = false;
14
+ let hasDot = false;
15
+ let isInExponent = false;
16
+ let canUseSign = true;
17
+
18
+ for (let i = 0; i < filtered.length; i++) {
19
+ const ch = filtered[i];
20
+
21
+ if (ch >= "0" && ch <= "9") {
22
+ result += ch;
23
+ canUseSign = false;
24
+ continue;
25
+ }
26
+
27
+ if (ch === ".") {
28
+ if (!isInExponent && !hasDot) {
29
+ result += ch;
30
+ hasDot = true;
31
+ }
32
+ continue;
33
+ }
34
+
35
+ if (ch === "e" || ch === "E") {
36
+ if (!hasExponent) {
37
+ if (/\d/.test(result)) {
38
+ result += ch;
39
+ hasExponent = true;
40
+ isInExponent = true;
41
+ canUseSign = true;
42
+ }
43
+ }
44
+ continue;
45
+ }
46
+
47
+ if (ch === "+" || ch === "-") {
48
+ if (canUseSign) {
49
+ result += ch;
50
+ canUseSign = false;
51
+ }
52
+ }
53
+ }
54
+
55
+ return result;
56
+ };