@jobber/components 8.25.1 → 8.26.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.
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
2
  import type { AtlantisThemeContextProviderProps, AtlantisThemeContextValue } from "./types";
3
3
  export declare const atlantisThemeContextDefaultValues: AtlantisThemeContextValue;
4
- export declare function AtlantisThemeContextProvider({ children, dangerouslyOverrideTheme, dangerouslyOverrideTokens, }: AtlantisThemeContextProviderProps): React.JSX.Element;
4
+ export declare function AtlantisThemeContextProvider({ children, dangerouslyOverrideTheme, dangerouslyOverrideTokens, storageKey, }: AtlantisThemeContextProviderProps): React.JSX.Element;
5
5
  export declare function useAtlantisTheme(): AtlantisThemeContextValue;
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  var AtlantisThemeContext = require('../AtlantisThemeContext-cjs.js');
4
- var updateTheme = require('../updateTheme-cjs.js');
5
4
  require('@jobber/design');
6
5
  require('react');
7
6
  require('../_commonjsHelpers-cjs.js');
@@ -19,5 +18,5 @@ require('../_setToString-cjs.js');
19
18
  exports.AtlantisThemeContextProvider = AtlantisThemeContext.AtlantisThemeContextProvider;
20
19
  exports.THEME_CHANGE_EVENT = AtlantisThemeContext.THEME_CHANGE_EVENT;
21
20
  exports.atlantisThemeContextDefaultValues = AtlantisThemeContext.atlantisThemeContextDefaultValues;
21
+ exports.updateTheme = AtlantisThemeContext.updateTheme;
22
22
  exports.useAtlantisTheme = AtlantisThemeContext.useAtlantisTheme;
23
- exports.updateTheme = updateTheme.updateTheme;
@@ -1,5 +1,4 @@
1
- export { A as AtlantisThemeContextProvider, T as THEME_CHANGE_EVENT, a as atlantisThemeContextDefaultValues, u as useAtlantisTheme } from '../AtlantisThemeContext-es.js';
2
- export { u as updateTheme } from '../updateTheme-es.js';
1
+ export { A as AtlantisThemeContextProvider, T as THEME_CHANGE_EVENT, a as atlantisThemeContextDefaultValues, b as updateTheme, u as useAtlantisTheme } from '../AtlantisThemeContext-es.js';
3
2
  import '@jobber/design';
4
3
  import 'react';
5
4
  import '../_commonjsHelpers-es.js';
@@ -0,0 +1,11 @@
1
+ import type { Theme } from "./types";
2
+ /**
3
+ * `useSyncExternalStore` source for the theme. `data-theme` on the document
4
+ * element is the source of truth: `updateTheme` writes it and dispatches
5
+ * `THEME_CHANGE_EVENT`, which this store subscribes to.
6
+ */
7
+ export declare const themeStore: {
8
+ readonly subscribe: (onStoreChange: () => void) => () => void;
9
+ readonly getSnapshot: () => Theme;
10
+ readonly getServerSnapshot: () => Theme;
11
+ };
@@ -32,6 +32,20 @@ export interface AtlantisThemeContextProviderProps extends PropsWithChildren {
32
32
  * **Important**: you should provide a constant or at least a memoized value for performance reasons.
33
33
  */
34
34
  readonly dangerouslyOverrideTokens?: OverrideTokens;
35
+ /**
36
+ * The `localStorage` key your app writes to when toggling the theme (e.g.
37
+ * `"jobber_theme"`). When set, the provider listens for `window` `storage`
38
+ * events on this key and applies theme changes signalled by other browser
39
+ * tabs. Writing to `localStorage` remains the app's responsibility; Atlantis
40
+ * only handles the listen-and-react side.
41
+ *
42
+ * Cross-tab sync is opt-in: omit this prop and no `storage` listener is added,
43
+ * preserving the previous within-tab-only behavior.
44
+ *
45
+ * Has no effect when `dangerouslyOverrideTheme` is set, since that provider
46
+ * is intentionally isolated from the shared theme store.
47
+ */
48
+ readonly storageKey?: string;
35
49
  }
36
50
  export type OverrideTokens = {
37
51
  [K in keyof typeof tokens]?: (typeof tokens)[K];
@@ -2,6 +2,6 @@ import type { Theme } from "./types";
2
2
  /**
3
3
  * Toggle the theme of the application. If subscribed, the tokens will be updated accordingly.
4
4
  * Will add data-theme="<newTheme>" to the <html> element.
5
- * Will dispatch a custom event "atlantis.toggle-theme" with the new theme for other providers to subscribe to.
5
+ * Will dispatch a custom event "atlantis.toggle-theme" that theme providers subscribe to.
6
6
  */
7
7
  export declare function updateTheme(theme: Theme): void;
@@ -340,9 +340,45 @@ var merge_1 = merge;
340
340
 
341
341
  var merge$1 = /*@__PURE__*/_commonjsHelpers.getDefaultExportFromCjs(merge_1);
342
342
 
343
+ var styles = {"staticThemeProviderWrapper":"P6HrDSaI0Ac-","overrideTokensWrapper":"j-NMCOBL3-E-","spinning":"iq03qkKnXBA-"};
344
+
343
345
  const THEME_CHANGE_EVENT = "atlantis.toggle-theme";
344
346
 
345
- var styles = {"staticThemeProviderWrapper":"P6HrDSaI0Ac-","overrideTokensWrapper":"j-NMCOBL3-E-","spinning":"iq03qkKnXBA-"};
347
+ /**
348
+ * `useSyncExternalStore` source for the theme. `data-theme` on the document
349
+ * element is the source of truth: `updateTheme` writes it and dispatches
350
+ * `THEME_CHANGE_EVENT`, which this store subscribes to.
351
+ */
352
+ const themeStore = {
353
+ subscribe(onStoreChange) {
354
+ globalThis.window.addEventListener(THEME_CHANGE_EVENT, onStoreChange);
355
+ return () => {
356
+ globalThis.window.removeEventListener(THEME_CHANGE_EVENT, onStoreChange);
357
+ };
358
+ },
359
+ getSnapshot() {
360
+ var _a, _b, _c, _d;
361
+ return ((_d = (_c = (_b = (_a = globalThis.document) === null || _a === void 0 ? void 0 : _a.documentElement) === null || _b === void 0 ? void 0 : _b.dataset) === null || _c === void 0 ? void 0 : _c.theme) !== null && _d !== void 0 ? _d : "light");
362
+ },
363
+ getServerSnapshot() {
364
+ return "light";
365
+ },
366
+ };
367
+
368
+ /**
369
+ * Toggle the theme of the application. If subscribed, the tokens will be updated accordingly.
370
+ * Will add data-theme="<newTheme>" to the <html> element.
371
+ * Will dispatch a custom event "atlantis.toggle-theme" that theme providers subscribe to.
372
+ */
373
+ function updateTheme(theme) {
374
+ if (!globalThis.window)
375
+ return;
376
+ globalThis.document.documentElement.setAttribute("data-theme", theme);
377
+ const event = new CustomEvent(THEME_CHANGE_EVENT, {
378
+ detail: { theme },
379
+ });
380
+ globalThis.window.dispatchEvent(event);
381
+ }
346
382
 
347
383
  const atlantisThemeContextDefaultValues = {
348
384
  theme: "light",
@@ -350,31 +386,35 @@ const atlantisThemeContextDefaultValues = {
350
386
  };
351
387
  const actualDarkTokens = merge$1({}, design.tokens, design.darkTokens);
352
388
  const AtlantisThemeContext = React.createContext(atlantisThemeContextDefaultValues);
353
- function AtlantisThemeContextProvider({ children, dangerouslyOverrideTheme, dangerouslyOverrideTokens, }) {
389
+ function AtlantisThemeContextProvider({ children, dangerouslyOverrideTheme, dangerouslyOverrideTokens, storageKey, }) {
354
390
  if (dangerouslyOverrideTheme) {
355
391
  return (React.createElement(InternalStaticThemeProvider, { dangerouslyOverrideTokens: dangerouslyOverrideTokens, dangerouslyOverrideTheme: dangerouslyOverrideTheme }, children));
356
392
  }
357
- return (React.createElement(InternalDynamicThemeProvider, { dangerouslyOverrideTokens: dangerouslyOverrideTokens }, children));
393
+ return (React.createElement(InternalDynamicThemeProvider, { dangerouslyOverrideTokens: dangerouslyOverrideTokens, storageKey: storageKey }, children));
358
394
  }
359
- function InternalDynamicThemeProvider({ children, dangerouslyOverrideTokens, }) {
360
- var _a;
361
- const initialTheme = (_a = globalThis.document.documentElement.dataset.theme) !== null && _a !== void 0 ? _a : "light";
362
- const [internalTheme, setInternalTheme] = React.useState(initialTheme);
363
- const { finalTokens, cssVariableOverrides } = useTokens(internalTheme, dangerouslyOverrideTokens);
364
- const handleThemeChangeEvent = React.useCallback((event) => {
365
- const newTheme = event.detail.theme;
366
- setInternalTheme(newTheme);
367
- }, []);
395
+ function InternalDynamicThemeProvider({ children, dangerouslyOverrideTokens, storageKey, }) {
396
+ const theme = React.useSyncExternalStore(themeStore.subscribe, themeStore.getSnapshot, themeStore.getServerSnapshot);
397
+ const { finalTokens, cssVariableOverrides } = useTokens(theme, dangerouslyOverrideTokens);
368
398
  React.useEffect(() => {
369
- if (!globalThis.window)
399
+ if (!storageKey || !globalThis.window)
370
400
  return;
371
- globalThis.window.addEventListener(THEME_CHANGE_EVENT, handleThemeChangeEvent);
401
+ function handleStorage(event) {
402
+ if (event.key !== storageKey)
403
+ return;
404
+ const next = event.newValue;
405
+ if (next !== "light" && next !== "dark")
406
+ return;
407
+ if (next === themeStore.getSnapshot())
408
+ return;
409
+ updateTheme(next);
410
+ }
411
+ globalThis.window.addEventListener("storage", handleStorage);
372
412
  return () => {
373
- globalThis.window.removeEventListener(THEME_CHANGE_EVENT, handleThemeChangeEvent);
413
+ globalThis.window.removeEventListener("storage", handleStorage);
374
414
  };
375
- }, [handleThemeChangeEvent]);
415
+ }, [storageKey]);
376
416
  return (React.createElement(AtlantisThemeContext.Provider, { value: {
377
- theme: internalTheme,
417
+ theme: theme,
378
418
  tokens: finalTokens,
379
419
  overrideTokens: dangerouslyOverrideTokens,
380
420
  } }, cssVariableOverrides ? (React.createElement("div", { className: styles.overrideTokensWrapper, style: cssVariableOverrides }, children)) : (children)));
@@ -419,4 +459,5 @@ function useTokens(theme, overrideTokens) {
419
459
  exports.AtlantisThemeContextProvider = AtlantisThemeContextProvider;
420
460
  exports.THEME_CHANGE_EVENT = THEME_CHANGE_EVENT;
421
461
  exports.atlantisThemeContextDefaultValues = atlantisThemeContextDefaultValues;
462
+ exports.updateTheme = updateTheme;
422
463
  exports.useAtlantisTheme = useAtlantisTheme;
@@ -1,11 +1,11 @@
1
1
  import { tokens, darkTokens } from '@jobber/design';
2
- import React__default, { createContext, useState, useCallback, useEffect, useContext, useMemo } from 'react';
2
+ import React__default, { createContext, useSyncExternalStore, useEffect, useContext, useMemo } from 'react';
3
3
  import { g as getDefaultExportFromCjs } from './_commonjsHelpers-es.js';
4
4
  import { e as eq_1, c as _Stack } from './identity-es.js';
5
5
  import { _ as _baseAssignValue } from './_baseAssignValue-es.js';
6
6
  import { _ as _baseFor } from './_baseFor-es.js';
7
7
  import { _ as _copyObject, k as keysIn_1, a as _cloneBufferExports, b as _cloneTypedArray, c as _copyArray, d as _initCloneObject, i as isPlainObject_1 } from './keysIn-es.js';
8
- import { b as isArrayLike_1, i as isArguments_1, a as isArray_1, c as isBufferExports, j as isFunction_1, d as isTypedArray_1 } from './isTypedArray-es.js';
8
+ import { b as isArrayLike_1, i as isArguments_1, a as isArray_1, c as isBufferExports, k as isFunction_1, d as isTypedArray_1 } from './isTypedArray-es.js';
9
9
  import { i as isObjectLike_1, a as isObject_1 } from './isObjectLike-es.js';
10
10
  import { _ as _baseRest, a as _isIterateeCall } from './_isIterateeCall-es.js';
11
11
 
@@ -338,9 +338,45 @@ var merge_1 = merge;
338
338
 
339
339
  var merge$1 = /*@__PURE__*/getDefaultExportFromCjs(merge_1);
340
340
 
341
+ var styles = {"staticThemeProviderWrapper":"P6HrDSaI0Ac-","overrideTokensWrapper":"j-NMCOBL3-E-","spinning":"iq03qkKnXBA-"};
342
+
341
343
  const THEME_CHANGE_EVENT = "atlantis.toggle-theme";
342
344
 
343
- var styles = {"staticThemeProviderWrapper":"P6HrDSaI0Ac-","overrideTokensWrapper":"j-NMCOBL3-E-","spinning":"iq03qkKnXBA-"};
345
+ /**
346
+ * `useSyncExternalStore` source for the theme. `data-theme` on the document
347
+ * element is the source of truth: `updateTheme` writes it and dispatches
348
+ * `THEME_CHANGE_EVENT`, which this store subscribes to.
349
+ */
350
+ const themeStore = {
351
+ subscribe(onStoreChange) {
352
+ globalThis.window.addEventListener(THEME_CHANGE_EVENT, onStoreChange);
353
+ return () => {
354
+ globalThis.window.removeEventListener(THEME_CHANGE_EVENT, onStoreChange);
355
+ };
356
+ },
357
+ getSnapshot() {
358
+ var _a, _b, _c, _d;
359
+ return ((_d = (_c = (_b = (_a = globalThis.document) === null || _a === void 0 ? void 0 : _a.documentElement) === null || _b === void 0 ? void 0 : _b.dataset) === null || _c === void 0 ? void 0 : _c.theme) !== null && _d !== void 0 ? _d : "light");
360
+ },
361
+ getServerSnapshot() {
362
+ return "light";
363
+ },
364
+ };
365
+
366
+ /**
367
+ * Toggle the theme of the application. If subscribed, the tokens will be updated accordingly.
368
+ * Will add data-theme="<newTheme>" to the <html> element.
369
+ * Will dispatch a custom event "atlantis.toggle-theme" that theme providers subscribe to.
370
+ */
371
+ function updateTheme(theme) {
372
+ if (!globalThis.window)
373
+ return;
374
+ globalThis.document.documentElement.setAttribute("data-theme", theme);
375
+ const event = new CustomEvent(THEME_CHANGE_EVENT, {
376
+ detail: { theme },
377
+ });
378
+ globalThis.window.dispatchEvent(event);
379
+ }
344
380
 
345
381
  const atlantisThemeContextDefaultValues = {
346
382
  theme: "light",
@@ -348,31 +384,35 @@ const atlantisThemeContextDefaultValues = {
348
384
  };
349
385
  const actualDarkTokens = merge$1({}, tokens, darkTokens);
350
386
  const AtlantisThemeContext = createContext(atlantisThemeContextDefaultValues);
351
- function AtlantisThemeContextProvider({ children, dangerouslyOverrideTheme, dangerouslyOverrideTokens, }) {
387
+ function AtlantisThemeContextProvider({ children, dangerouslyOverrideTheme, dangerouslyOverrideTokens, storageKey, }) {
352
388
  if (dangerouslyOverrideTheme) {
353
389
  return (React__default.createElement(InternalStaticThemeProvider, { dangerouslyOverrideTokens: dangerouslyOverrideTokens, dangerouslyOverrideTheme: dangerouslyOverrideTheme }, children));
354
390
  }
355
- return (React__default.createElement(InternalDynamicThemeProvider, { dangerouslyOverrideTokens: dangerouslyOverrideTokens }, children));
391
+ return (React__default.createElement(InternalDynamicThemeProvider, { dangerouslyOverrideTokens: dangerouslyOverrideTokens, storageKey: storageKey }, children));
356
392
  }
357
- function InternalDynamicThemeProvider({ children, dangerouslyOverrideTokens, }) {
358
- var _a;
359
- const initialTheme = (_a = globalThis.document.documentElement.dataset.theme) !== null && _a !== void 0 ? _a : "light";
360
- const [internalTheme, setInternalTheme] = useState(initialTheme);
361
- const { finalTokens, cssVariableOverrides } = useTokens(internalTheme, dangerouslyOverrideTokens);
362
- const handleThemeChangeEvent = useCallback((event) => {
363
- const newTheme = event.detail.theme;
364
- setInternalTheme(newTheme);
365
- }, []);
393
+ function InternalDynamicThemeProvider({ children, dangerouslyOverrideTokens, storageKey, }) {
394
+ const theme = useSyncExternalStore(themeStore.subscribe, themeStore.getSnapshot, themeStore.getServerSnapshot);
395
+ const { finalTokens, cssVariableOverrides } = useTokens(theme, dangerouslyOverrideTokens);
366
396
  useEffect(() => {
367
- if (!globalThis.window)
397
+ if (!storageKey || !globalThis.window)
368
398
  return;
369
- globalThis.window.addEventListener(THEME_CHANGE_EVENT, handleThemeChangeEvent);
399
+ function handleStorage(event) {
400
+ if (event.key !== storageKey)
401
+ return;
402
+ const next = event.newValue;
403
+ if (next !== "light" && next !== "dark")
404
+ return;
405
+ if (next === themeStore.getSnapshot())
406
+ return;
407
+ updateTheme(next);
408
+ }
409
+ globalThis.window.addEventListener("storage", handleStorage);
370
410
  return () => {
371
- globalThis.window.removeEventListener(THEME_CHANGE_EVENT, handleThemeChangeEvent);
411
+ globalThis.window.removeEventListener("storage", handleStorage);
372
412
  };
373
- }, [handleThemeChangeEvent]);
413
+ }, [storageKey]);
374
414
  return (React__default.createElement(AtlantisThemeContext.Provider, { value: {
375
- theme: internalTheme,
415
+ theme: theme,
376
416
  tokens: finalTokens,
377
417
  overrideTokens: dangerouslyOverrideTokens,
378
418
  } }, cssVariableOverrides ? (React__default.createElement("div", { className: styles.overrideTokensWrapper, style: cssVariableOverrides }, children)) : (children)));
@@ -414,4 +454,4 @@ function useTokens(theme, overrideTokens) {
414
454
  };
415
455
  }
416
456
 
417
- export { AtlantisThemeContextProvider as A, THEME_CHANGE_EVENT as T, atlantisThemeContextDefaultValues as a, useAtlantisTheme as u };
457
+ export { AtlantisThemeContextProvider as A, THEME_CHANGE_EVENT as T, atlantisThemeContextDefaultValues as a, updateTheme as b, useAtlantisTheme as u };
@@ -13,7 +13,7 @@ import { c as _Stack } from './identity-es.js';
13
13
  import { _ as _copyObject, k as keysIn_1, e as _getPrototype, f as _cloneArrayBuffer, b as _cloneTypedArray, a as _cloneBufferExports, c as _copyArray, d as _initCloneObject, g as _assignValue, i as isPlainObject_1 } from './keysIn-es.js';
14
14
  import { a as _getTag } from './_getTag-es.js';
15
15
  import { b as _Symbol, i as isObjectLike_1, a as isObject_1 } from './isObjectLike-es.js';
16
- import { m as _nodeUtilExports, k as _baseUnary, c as isBufferExports, a as isArray_1 } from './isTypedArray-es.js';
16
+ import { m as _nodeUtilExports, j as _baseUnary, c as isBufferExports, a as isArray_1 } from './isTypedArray-es.js';
17
17
  import { _ as _baseFlatten } from './_baseFlatten-es.js';
18
18
  import { _ as _overRest, a as _setToString } from './_setToString-es.js';
19
19
  import { u as useAtlantisContext } from './AtlantisContext-es.js';
@@ -901,8 +901,9 @@ function useInputNumberExperimentalContext(consumer) {
901
901
  }
902
902
  return context;
903
903
  }
904
- function InputNumberExperimentalWrapperInternal(props, ref) {
905
- const { align, autoComplete, children, className, disabled, format, id: idProp, inline, inputMode, invalid, loading, max, maxLength, min, name, onBlur, onEnter, onFocus, onKeyDown, onKeyUp, onValueChange, onValueCommitted, readOnly, showMiniLabel = true, size = "default", step = 1, style, value, } = props;
904
+ function InputNumberExperimentalWrapper(_a) {
905
+ var { ref } = _a, props = tslib_es6.__rest(_a, ["ref"]);
906
+ const { align, autoComplete, children, className, disabled, format, id: idProp, inline, inputMode, invalid, loading, max, min, name, onBlur, onEnter, onFocus, onKeyDown, onKeyUp, onValueChange, onValueCommitted, readOnly, showMiniLabel = true, size = "default", step = 1, style, value, } = props;
906
907
  const generatedId = React.useId();
907
908
  const id = idProp !== null && idProp !== void 0 ? idProp : generatedId;
908
909
  const innerInputRef = React.useRef(null);
@@ -938,7 +939,6 @@ function InputNumberExperimentalWrapperInternal(props, ref) {
938
939
  inputMode,
939
940
  loading,
940
941
  max,
941
- maxLength,
942
942
  min,
943
943
  onBlur,
944
944
  onFocus,
@@ -961,7 +961,6 @@ function InputNumberExperimentalWrapperInternal(props, ref) {
961
961
  inputMode,
962
962
  loading,
963
963
  max,
964
- maxLength,
965
964
  min,
966
965
  onBlur,
967
966
  onFocus,
@@ -999,7 +998,7 @@ function InputNumberExperimentalInput({ children, className, style, }) {
999
998
  className: classnames(styles.inputWrapper, !ctx.showMiniLabel && styles.hideLabel, ctx.size !== "default" && styles[ctx.size], ctx.disabled && styles.disabled),
1000
999
  }, { className, style });
1001
1000
  return (React.createElement("div", Object.assign({}, rootProps),
1002
- React.createElement(NumberFieldInput.NumberFieldInput, { "aria-busy": ctx.loading || undefined, autoComplete: resolveAutoComplete(ctx.autoComplete), className: styles.input, inputMode: ctx.inputMode, maxLength: ctx.maxLength, onBlur: ctx.onBlur, onFocus: ctx.onFocus, onKeyDown: ctx.onKeyDown, onKeyUp: ctx.onKeyUp, placeholder: " ", ref: ctx.innerInputRef }),
1001
+ React.createElement(NumberFieldInput.NumberFieldInput, { "aria-busy": ctx.loading || undefined, autoComplete: resolveAutoComplete(ctx.autoComplete), className: styles.input, inputMode: ctx.inputMode, onBlur: ctx.onBlur, onFocus: ctx.onFocus, onKeyDown: ctx.onKeyDown, onKeyUp: ctx.onKeyUp, placeholder: " ", ref: ctx.innerInputRef }),
1003
1002
  children));
1004
1003
  }
1005
1004
  function InputNumberExperimentalLabel({ children, className, style, }) {
@@ -1099,10 +1098,11 @@ function resolveAutoComplete(autoComplete) {
1099
1098
  * consumer would write by hand with `<InputNumberExperimental.Wrapper>` and the
1100
1099
  * parts — to customize a single piece, copy this tree and swap that part.
1101
1100
  */
1102
- function InputNumberExperimentalInternal(props, ref) {
1103
- var _a;
1101
+ function InputNumberExperimental(_a) {
1102
+ var _b;
1103
+ var { ref } = _a, props = tslib_es6.__rest(_a, ["ref"]);
1104
1104
  const { label, description, error, prefix, suffix } = props, config = tslib_es6.__rest(props, ["label", "description", "error", "prefix", "suffix"]);
1105
- const size = (_a = config.size) !== null && _a !== void 0 ? _a : "default";
1105
+ const size = (_b = config.size) !== null && _b !== void 0 ? _b : "default";
1106
1106
  return (React.createElement(InputNumberExperimentalWrapper, Object.assign({}, config, { invalid: config.invalid || Boolean(error), ref: ref }),
1107
1107
  React.createElement(InputNumberExperimentalGroup, null,
1108
1108
  prefix && renderDefaultAffix("prefix", prefix, size),
@@ -1114,10 +1114,8 @@ function InputNumberExperimentalInternal(props, ref) {
1114
1114
  React.createElement(InputNumberExperimentalDescription, null, description),
1115
1115
  React.createElement(InputNumberExperimentalError, null, error)))));
1116
1116
  }
1117
- const InputNumberExperimentalWrapper = React.forwardRef(InputNumberExperimentalWrapperInternal);
1118
1117
  InputNumberExperimentalWrapper.displayName = "InputNumberExperimental.Wrapper";
1119
- const InputNumberExperimentalBase = React.forwardRef(InputNumberExperimentalInternal);
1120
- InputNumberExperimentalBase.displayName = "InputNumberExperimental";
1118
+ InputNumberExperimental.displayName = "InputNumberExperimental";
1121
1119
  InputNumberExperimentalGroup.displayName = "InputNumberExperimental.Group";
1122
1120
  InputNumberExperimentalFooter.displayName = "InputNumberExperimental.Footer";
1123
1121
  InputNumberExperimentalInput.displayName = "InputNumberExperimental.Input";
@@ -1133,19 +1131,17 @@ InputNumberExperimentalDecrement.displayName =
1133
1131
  "InputNumberExperimental.Decrement";
1134
1132
  InputNumberExperimentalAffixCompound.displayName =
1135
1133
  "InputNumberExperimental.Affix";
1136
- const InputNumberExperimental = Object.assign(InputNumberExperimentalBase, {
1137
- Wrapper: InputNumberExperimentalWrapper,
1138
- Group: InputNumberExperimentalGroup,
1139
- Footer: InputNumberExperimentalFooter,
1140
- Input: InputNumberExperimentalInput,
1141
- Label: InputNumberExperimentalLabel,
1142
- Description: InputNumberExperimentalDescription,
1143
- Error: InputNumberExperimentalError,
1144
- Loading: InputNumberExperimentalLoading,
1145
- Stepper: InputNumberExperimentalStepper,
1146
- Increment: InputNumberExperimentalIncrement,
1147
- Decrement: InputNumberExperimentalDecrement,
1148
- Affix: InputNumberExperimentalAffixCompound,
1149
- });
1134
+ InputNumberExperimental.Wrapper = InputNumberExperimentalWrapper;
1135
+ InputNumberExperimental.Group = InputNumberExperimentalGroup;
1136
+ InputNumberExperimental.Footer = InputNumberExperimentalFooter;
1137
+ InputNumberExperimental.Input = InputNumberExperimentalInput;
1138
+ InputNumberExperimental.Label = InputNumberExperimentalLabel;
1139
+ InputNumberExperimental.Description = InputNumberExperimentalDescription;
1140
+ InputNumberExperimental.Error = InputNumberExperimentalError;
1141
+ InputNumberExperimental.Loading = InputNumberExperimentalLoading;
1142
+ InputNumberExperimental.Stepper = InputNumberExperimentalStepper;
1143
+ InputNumberExperimental.Increment = InputNumberExperimentalIncrement;
1144
+ InputNumberExperimental.Decrement = InputNumberExperimentalDecrement;
1145
+ InputNumberExperimental.Affix = InputNumberExperimentalAffixCompound;
1150
1146
 
1151
1147
  exports.InputNumberExperimental = InputNumberExperimental;
@@ -1,6 +1,6 @@
1
1
  import { _ as __rest } from './tslib.es6-es.js';
2
2
  import * as React from 'react';
3
- import React__default, { createContext, forwardRef, useId, useRef, useImperativeHandle, useCallback, useMemo, useContext } from 'react';
3
+ import React__default, { createContext, useId, useRef, useImperativeHandle, useCallback, useMemo, useContext } from 'react';
4
4
  import classnames from 'classnames';
5
5
  import { A as ActivityIndicator } from './ActivityIndicator-es.js';
6
6
  import { B as Button } from './Button-es.js';
@@ -881,8 +881,9 @@ function useInputNumberExperimentalContext(consumer) {
881
881
  }
882
882
  return context;
883
883
  }
884
- function InputNumberExperimentalWrapperInternal(props, ref) {
885
- const { align, autoComplete, children, className, disabled, format, id: idProp, inline, inputMode, invalid, loading, max, maxLength, min, name, onBlur, onEnter, onFocus, onKeyDown, onKeyUp, onValueChange, onValueCommitted, readOnly, showMiniLabel = true, size = "default", step = 1, style, value, } = props;
884
+ function InputNumberExperimentalWrapper(_a) {
885
+ var { ref } = _a, props = __rest(_a, ["ref"]);
886
+ const { align, autoComplete, children, className, disabled, format, id: idProp, inline, inputMode, invalid, loading, max, min, name, onBlur, onEnter, onFocus, onKeyDown, onKeyUp, onValueChange, onValueCommitted, readOnly, showMiniLabel = true, size = "default", step = 1, style, value, } = props;
886
887
  const generatedId = useId();
887
888
  const id = idProp !== null && idProp !== void 0 ? idProp : generatedId;
888
889
  const innerInputRef = useRef(null);
@@ -918,7 +919,6 @@ function InputNumberExperimentalWrapperInternal(props, ref) {
918
919
  inputMode,
919
920
  loading,
920
921
  max,
921
- maxLength,
922
922
  min,
923
923
  onBlur,
924
924
  onFocus,
@@ -941,7 +941,6 @@ function InputNumberExperimentalWrapperInternal(props, ref) {
941
941
  inputMode,
942
942
  loading,
943
943
  max,
944
- maxLength,
945
944
  min,
946
945
  onBlur,
947
946
  onFocus,
@@ -979,7 +978,7 @@ function InputNumberExperimentalInput({ children, className, style, }) {
979
978
  className: classnames(styles.inputWrapper, !ctx.showMiniLabel && styles.hideLabel, ctx.size !== "default" && styles[ctx.size], ctx.disabled && styles.disabled),
980
979
  }, { className, style });
981
980
  return (React__default.createElement("div", Object.assign({}, rootProps),
982
- React__default.createElement(NumberFieldInput, { "aria-busy": ctx.loading || undefined, autoComplete: resolveAutoComplete(ctx.autoComplete), className: styles.input, inputMode: ctx.inputMode, maxLength: ctx.maxLength, onBlur: ctx.onBlur, onFocus: ctx.onFocus, onKeyDown: ctx.onKeyDown, onKeyUp: ctx.onKeyUp, placeholder: " ", ref: ctx.innerInputRef }),
981
+ React__default.createElement(NumberFieldInput, { "aria-busy": ctx.loading || undefined, autoComplete: resolveAutoComplete(ctx.autoComplete), className: styles.input, inputMode: ctx.inputMode, onBlur: ctx.onBlur, onFocus: ctx.onFocus, onKeyDown: ctx.onKeyDown, onKeyUp: ctx.onKeyUp, placeholder: " ", ref: ctx.innerInputRef }),
983
982
  children));
984
983
  }
985
984
  function InputNumberExperimentalLabel({ children, className, style, }) {
@@ -1079,10 +1078,11 @@ function resolveAutoComplete(autoComplete) {
1079
1078
  * consumer would write by hand with `<InputNumberExperimental.Wrapper>` and the
1080
1079
  * parts — to customize a single piece, copy this tree and swap that part.
1081
1080
  */
1082
- function InputNumberExperimentalInternal(props, ref) {
1083
- var _a;
1081
+ function InputNumberExperimental(_a) {
1082
+ var _b;
1083
+ var { ref } = _a, props = __rest(_a, ["ref"]);
1084
1084
  const { label, description, error, prefix, suffix } = props, config = __rest(props, ["label", "description", "error", "prefix", "suffix"]);
1085
- const size = (_a = config.size) !== null && _a !== void 0 ? _a : "default";
1085
+ const size = (_b = config.size) !== null && _b !== void 0 ? _b : "default";
1086
1086
  return (React__default.createElement(InputNumberExperimentalWrapper, Object.assign({}, config, { invalid: config.invalid || Boolean(error), ref: ref }),
1087
1087
  React__default.createElement(InputNumberExperimentalGroup, null,
1088
1088
  prefix && renderDefaultAffix("prefix", prefix, size),
@@ -1094,10 +1094,8 @@ function InputNumberExperimentalInternal(props, ref) {
1094
1094
  React__default.createElement(InputNumberExperimentalDescription, null, description),
1095
1095
  React__default.createElement(InputNumberExperimentalError, null, error)))));
1096
1096
  }
1097
- const InputNumberExperimentalWrapper = forwardRef(InputNumberExperimentalWrapperInternal);
1098
1097
  InputNumberExperimentalWrapper.displayName = "InputNumberExperimental.Wrapper";
1099
- const InputNumberExperimentalBase = forwardRef(InputNumberExperimentalInternal);
1100
- InputNumberExperimentalBase.displayName = "InputNumberExperimental";
1098
+ InputNumberExperimental.displayName = "InputNumberExperimental";
1101
1099
  InputNumberExperimentalGroup.displayName = "InputNumberExperimental.Group";
1102
1100
  InputNumberExperimentalFooter.displayName = "InputNumberExperimental.Footer";
1103
1101
  InputNumberExperimentalInput.displayName = "InputNumberExperimental.Input";
@@ -1113,19 +1111,17 @@ InputNumberExperimentalDecrement.displayName =
1113
1111
  "InputNumberExperimental.Decrement";
1114
1112
  InputNumberExperimentalAffixCompound.displayName =
1115
1113
  "InputNumberExperimental.Affix";
1116
- const InputNumberExperimental = Object.assign(InputNumberExperimentalBase, {
1117
- Wrapper: InputNumberExperimentalWrapper,
1118
- Group: InputNumberExperimentalGroup,
1119
- Footer: InputNumberExperimentalFooter,
1120
- Input: InputNumberExperimentalInput,
1121
- Label: InputNumberExperimentalLabel,
1122
- Description: InputNumberExperimentalDescription,
1123
- Error: InputNumberExperimentalError,
1124
- Loading: InputNumberExperimentalLoading,
1125
- Stepper: InputNumberExperimentalStepper,
1126
- Increment: InputNumberExperimentalIncrement,
1127
- Decrement: InputNumberExperimentalDecrement,
1128
- Affix: InputNumberExperimentalAffixCompound,
1129
- });
1114
+ InputNumberExperimental.Wrapper = InputNumberExperimentalWrapper;
1115
+ InputNumberExperimental.Group = InputNumberExperimentalGroup;
1116
+ InputNumberExperimental.Footer = InputNumberExperimentalFooter;
1117
+ InputNumberExperimental.Input = InputNumberExperimentalInput;
1118
+ InputNumberExperimental.Label = InputNumberExperimentalLabel;
1119
+ InputNumberExperimental.Description = InputNumberExperimentalDescription;
1120
+ InputNumberExperimental.Error = InputNumberExperimentalError;
1121
+ InputNumberExperimental.Loading = InputNumberExperimentalLoading;
1122
+ InputNumberExperimental.Stepper = InputNumberExperimentalStepper;
1123
+ InputNumberExperimental.Increment = InputNumberExperimentalIncrement;
1124
+ InputNumberExperimental.Decrement = InputNumberExperimentalDecrement;
1125
+ InputNumberExperimental.Affix = InputNumberExperimentalAffixCompound;
1130
1126
 
1131
1127
  export { InputNumberExperimental as I };
@@ -116,6 +116,46 @@ function ThemedComponent() {
116
116
  }
117
117
  ```
118
118
 
119
+ ### Cross-tab theme sync (Web Only)
120
+
121
+ The provider can keep the theme in sync across a user's open browser tabs. This
122
+ is opt-in: pass the `localStorage` key your app writes to as the `storageKey`
123
+ prop. The provider then listens for
124
+ [`storage` events](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event)
125
+ on that key and applies theme changes from other tabs. The `storage` event fires
126
+ only in tabs *other* than the one that wrote the value, so there's no feedback
127
+ loop.
128
+
129
+ **Your app is responsible for writing to `localStorage`** when the theme changes
130
+ — Atlantis only handles the listen-and-react side.
131
+
132
+ ```tsx
133
+ <AtlantisThemeContextProvider storageKey="my_app_theme">
134
+ <ThemedComponent />
135
+ </AtlantisThemeContextProvider>
136
+ ```
137
+
138
+ ```tsx
139
+ // Theme toggle — write to the same key after calling updateTheme
140
+ import { updateTheme } from "@jobber/components/AtlantisThemeContext";
141
+
142
+ function toggleTheme(newTheme: "light" | "dark") {
143
+ updateTheme(newTheme); // updates all providers in this tab immediately
144
+
145
+ try {
146
+ localStorage.setItem("my_app_theme", newTheme); // signals other tabs
147
+ } catch {
148
+ // localStorage unavailable (e.g. private mode) — degrades gracefully
149
+ }
150
+ }
151
+ ```
152
+
153
+ Omit `storageKey` and no `storage` listener is added, keeping the previous
154
+ within-tab-only behavior. Multiple dynamic providers on the same page all share
155
+ the same internal store, so a single provider with a `storageKey` is enough — a
156
+ cross-tab signal updates every dynamic provider at once. Providers mounted with
157
+ `dangerouslyOverrideTheme` are unaffected.
158
+
119
159
  ### Forcing a theme for an AtlantisThemeContextProvider
120
160
 
121
161
  In some scenarios you may want to force a theme for specific components
@@ -255,3 +295,4 @@ Key features:
255
295
  | `children` | `ReactNode` | Yes | — | The children to render. |
256
296
  | `dangerouslyOverrideTheme` | `Theme` | No | — | Force the theme for this provider to always be the same as the provided theme. Useful for sections that should remain... |
257
297
  | `dangerouslyOverrideTokens` | `OverrideTokens` | No | — | Overrides existing design tokens with custom values. Can also supply custom tokens which will be accessible via useAt... |
298
+ | `storageKey` | `string` | No | — | The `localStorage` key your app writes to when toggling the theme (e.g. `"jobber_theme"`). When set, the provider lis... |
@@ -0,0 +1,519 @@
1
+ # InputNumberExperimental
2
+
3
+ ## Summary
4
+
5
+ `InputNumberExperimental` collects a single numeric value in a form. Reach for
6
+ it when the value benefits from being nudged up or down — quantities, prices,
7
+ durations, or counts.
8
+
9
+ Most fields need only the prop-driven usage below. For the rare layout the props
10
+ can't express, the same field can be composed from parts — see the **Implement**
11
+ tab.
12
+
13
+ ### When to use
14
+
15
+ * The value is a number the user increments or decrements (quantity, price,
16
+ days, repetitions).
17
+ * A stepper, min/max bounds, or number formatting would help the user.
18
+
19
+ ### When not to use
20
+
21
+ * The value is a sequence of digits that is never calculated with — phone
22
+ numbers, credit-card numbers, or postal codes. A stepper adds no value there;
23
+ use [InputText](../InputText/InputText.md) instead.
24
+
25
+ ## Basic usage
26
+
27
+ Pass `label`, a controlled `value`, and `onValueCommitted`. Bounds (`min` /
28
+ `max`) and `step` are optional.
29
+
30
+ ```tsx
31
+ import React, { useState } from "react";
32
+ import type { InputNumberExperimentalProps } from "@jobber/components/primitives";
33
+ import { InputNumberExperimental } from "@jobber/components/primitives";
34
+
35
+ export function InputNumberExperimentalBasicExample(
36
+ props: Partial<InputNumberExperimentalProps>,
37
+ ) {
38
+ const [value, setValue] = useState<number | null>(3);
39
+
40
+ return (
41
+ <InputNumberExperimental
42
+ label="Quantity"
43
+ min={0}
44
+ max={100}
45
+ {...props}
46
+ value={value}
47
+ onValueCommitted={setValue}
48
+ />
49
+ );
50
+ }
51
+ ```
52
+
53
+ ## Options
54
+
55
+ ### Prefixes and suffixes
56
+
57
+ Use `prefix` / `suffix` to add a unit or symbol. A suffix can be a label, an
58
+ icon, or a clickable icon that runs an action (give it an `ariaLabel`).
59
+
60
+ ```tsx
61
+ import React, { useState } from "react";
62
+ import { InputNumberExperimental } from "@jobber/components/primitives";
63
+ import { Content } from "@jobber/components/Content";
64
+
65
+ export function InputNumberExperimentalAffixesExample() {
66
+ const [price, setPrice] = useState<number | null>(42);
67
+ const [days, setDays] = useState<number | null>(7);
68
+ const [reps, setReps] = useState<number | null>(3);
69
+
70
+ return (
71
+ <Content>
72
+ <InputNumberExperimental
73
+ label="Price"
74
+ prefix={{ label: "$" }}
75
+ suffix={{ label: "USD" }}
76
+ value={price}
77
+ onValueCommitted={setPrice}
78
+ />
79
+
80
+ <InputNumberExperimental
81
+ label="Follow-up in"
82
+ suffix={{ icon: "calendar", label: "days" }}
83
+ value={days}
84
+ onValueCommitted={setDays}
85
+ />
86
+
87
+ <InputNumberExperimental
88
+ label="Repetitions"
89
+ suffix={{
90
+ icon: "remove",
91
+ ariaLabel: "Clear value",
92
+ onClick: () => setReps(null),
93
+ }}
94
+ value={reps}
95
+ onValueCommitted={setReps}
96
+ />
97
+ </Content>
98
+ );
99
+ }
100
+ ```
101
+
102
+ ### Sizes
103
+
104
+ Three sizes are available. `default` fits almost every form; use `small` only in
105
+ tight spaces and `large` only in especially spacious layouts.
106
+
107
+ ```tsx
108
+ import React, { useState } from "react";
109
+ import { InputNumberExperimental } from "@jobber/components/primitives";
110
+ import { Content } from "@jobber/components/Content";
111
+
112
+ export function InputNumberExperimentalSizesExample() {
113
+ const [small, setSmall] = useState<number | null>(42);
114
+ const [base, setBase] = useState<number | null>(42);
115
+ const [large, setLarge] = useState<number | null>(42);
116
+
117
+ return (
118
+ <Content>
119
+ <InputNumberExperimental
120
+ label="Small"
121
+ size="small"
122
+ suffix={{ label: "items" }}
123
+ value={small}
124
+ onValueCommitted={setSmall}
125
+ />
126
+ <InputNumberExperimental
127
+ label="Default"
128
+ size="default"
129
+ suffix={{ label: "items" }}
130
+ value={base}
131
+ onValueCommitted={setBase}
132
+ />
133
+ <InputNumberExperimental
134
+ label="Large"
135
+ size="large"
136
+ suffix={{ label: "items" }}
137
+ value={large}
138
+ onValueCommitted={setLarge}
139
+ />
140
+ </Content>
141
+ );
142
+ }
143
+ ```
144
+
145
+ ### Formatting
146
+
147
+ `format` takes any `Intl.NumberFormatOptions` and controls only how the value is
148
+ displayed; the committed value stays a plain number. See the **Implement** tab
149
+ for how percent and currency values map to the underlying number.
150
+
151
+ ```tsx
152
+ import React, { useState } from "react";
153
+ import { InputNumberExperimental } from "@jobber/components/primitives";
154
+ import { Content } from "@jobber/components/Content";
155
+
156
+ export function InputNumberExperimentalFormattingExample() {
157
+ const [currency, setCurrency] = useState<number | null>(1234.5);
158
+ const [percent, setPercent] = useState<number | null>(0.5);
159
+ const [decimal, setDecimal] = useState<number | null>(11.13);
160
+
161
+ return (
162
+ <Content>
163
+ <InputNumberExperimental
164
+ label="Currency"
165
+ description='{ style: "currency", currency: "USD" }'
166
+ format={{ style: "currency", currency: "USD" }}
167
+ value={currency}
168
+ onValueCommitted={setCurrency}
169
+ />
170
+ <InputNumberExperimental
171
+ label="Percent"
172
+ description='{ style: "percent" } — value is a ratio: 0.5 → 50%'
173
+ format={{ style: "percent", maximumFractionDigits: 2 }}
174
+ value={percent}
175
+ onValueCommitted={setPercent}
176
+ />
177
+ <InputNumberExperimental
178
+ label="Decimal"
179
+ description="{ maximumFractionDigits: 2 }"
180
+ format={{ maximumFractionDigits: 2 }}
181
+ value={decimal}
182
+ onValueCommitted={setDecimal}
183
+ />
184
+ </Content>
185
+ );
186
+ }
187
+ ```
188
+
189
+ ### Loading
190
+
191
+ `loading` shows a non-blocking indicator in the stepper's slot for background
192
+ work (for example, saving). The field stays editable and the stepper is hidden
193
+ while loading.
194
+
195
+ ```tsx
196
+ import React, { useState } from "react";
197
+ import { InputNumberExperimental } from "@jobber/components/primitives";
198
+
199
+ export function InputNumberExperimentalLoadingExample() {
200
+ const [value, setValue] = useState<number | null>(42);
201
+
202
+ return (
203
+ <InputNumberExperimental
204
+ loading
205
+ label="Quantity"
206
+ suffix={{ label: "items" }}
207
+ value={value}
208
+ onValueCommitted={setValue}
209
+ />
210
+ );
211
+ }
212
+ ```
213
+
214
+ ## Content guidelines
215
+
216
+ ### Label the unit, don't repeat it
217
+
218
+ Put the unit in the label or an affix, not both.
219
+
220
+ | ✅ Do | ❌ Don't |
221
+ | ------------------------------- | --------------------------------------- |
222
+ | Label "Weight", suffix "kg" | Label "Weight (kg)", suffix "kg" |
223
+ | Label "Duration", suffix "days" | Label "Duration in days", suffix "days" |
224
+
225
+ ### Keep labels short and sentence-cased
226
+
227
+ | ✅ Do | ❌ Don't |
228
+ | -------- | ----------------------- |
229
+ | Quantity | Enter the quantity here |
230
+ | Discount | DISCOUNT % |
231
+
232
+ ## Do's and Don'ts
233
+
234
+ * ✅ Use for values the user increments or decrements
235
+ * ✅ Set `min` / `max` when the value has real bounds
236
+ * ✅ Use `format` for currency, percent, and decimals rather than formatting the
237
+ value yourself
238
+ * ❌ Don't use it for digit sequences that are never calculated with (phone,
239
+ credit card)
240
+ * ❌ Don't disable the field to communicate an error — show an `error` message
241
+ instead
242
+
243
+ ## Accessibility
244
+
245
+ The field is a native number input, so it is reachable and operable by keyboard
246
+ and assistive technology.
247
+
248
+ | Key | Behavior |
249
+ | ---------------- | ------------------------------- |
250
+ | Tab | Moves focus to the field |
251
+ | Up / Down arrows | Increment / decrement by `step` |
252
+ | Enter | Commits the current value |
253
+ | Type | Replaces the value |
254
+
255
+ Give a clickable affix a clear `ariaLabel` describing its action (for example,
256
+ "Clear value").
257
+
258
+ ## Related components
259
+
260
+ * For digit sequences that are not calculated with (phone, credit card), use
261
+ [InputText](../InputText/InputText.md).
262
+ * For dates, use [InputDate](../InputDate/InputDate.md).
263
+
264
+
265
+ ## Anatomy
266
+
267
+ The prop-driven `<InputNumberExperimental>` composes a set of parts. You only
268
+ need these when the props can't express a layout; otherwise reach for the props
269
+ shown on the **Design** tab.
270
+
271
+ | Part | Description |
272
+ | ----------------------- | -------------------------------------------------------------------- |
273
+ | `Wrapper` | Owns the field configuration and state; provides it to the parts |
274
+ | `Group` | The bordered field row |
275
+ | `Input` | The input area; holds the `Label` and the `Stepper` / `Loading` slot |
276
+ | `Label` | Floating field label |
277
+ | `Stepper` | The increment / decrement button pair |
278
+ | `Increment` `Decrement` | The individual stepper buttons |
279
+ | `Affix` | Prefix / suffix content (label, icon, or clickable icon) |
280
+ | `Loading` | Non-blocking loading indicator slot |
281
+ | `Footer` | Below-field row that holds `Description` and `Error` |
282
+ | `Description` | Helper text below the field |
283
+ | `Error` | Styled error message below the field |
284
+
285
+ ## Composition
286
+
287
+ The prop-driven component is sugar: it renders exactly the tree you would write
288
+ by hand with `<InputNumberExperimental.Wrapper>` and the parts. To customize a
289
+ single piece, compose the tree yourself and swap that one part — the other parts
290
+ keep their defaults. The sugar does not merge consumer-provided parts into its
291
+ render, so there is no per-slot precedence to reason about.
292
+
293
+ `Wrapper` owns the field state and shares it with the parts through context, so
294
+ every part must be rendered inside a `Wrapper` (a part used outside one throws).
295
+
296
+ The example below replaces the default stepper icons with `+` / `−` and leaves
297
+ everything else as the default:
298
+
299
+ ```tsx
300
+ import React, { useState } from "react";
301
+ import { InputNumberExperimental } from "@jobber/components/primitives";
302
+
303
+ export function InputNumberExperimentalCompositionExample() {
304
+ const [value, setValue] = useState<number | null>(3);
305
+
306
+ return (
307
+ <InputNumberExperimental.Wrapper value={value} onValueCommitted={setValue}>
308
+ <InputNumberExperimental.Group>
309
+ <InputNumberExperimental.Input>
310
+ <InputNumberExperimental.Label>
311
+ Quantity
312
+ </InputNumberExperimental.Label>
313
+ <InputNumberExperimental.Stepper>
314
+ <InputNumberExperimental.Increment ariaLabel="Increase Quantity">
315
+ +
316
+ </InputNumberExperimental.Increment>
317
+ <InputNumberExperimental.Decrement ariaLabel="Decrease Quantity">
318
+
319
+ </InputNumberExperimental.Decrement>
320
+ </InputNumberExperimental.Stepper>
321
+ </InputNumberExperimental.Input>
322
+ </InputNumberExperimental.Group>
323
+ </InputNumberExperimental.Wrapper>
324
+ );
325
+ }
326
+ ```
327
+
328
+ ## Controlled usage
329
+
330
+ The field is controlled: pass `value` (a `number`, or `null` for empty) and read
331
+ changes back through one of two callbacks.
332
+
333
+ | Callback | Fires | Use for |
334
+ | ------------------ | ----------------------------------------------------------- | ---------------------------------- |
335
+ | `onValueChange` | On every parsed change (typing, paste, stepper, arrow step) | Live-updating UI as the user types |
336
+ | `onValueCommitted` | When the user commits (blur, Enter, stepper, arrow step) | Saving / validating a final value |
337
+
338
+ Both emit `null` when the field is empty. Prefer `onValueCommitted` for
339
+ persistence so you are not writing on every keystroke.
340
+
341
+ ## Formatting semantics
342
+
343
+ `format` is forwarded to Base UI's `NumberField` `format` and accepts any
344
+ `Intl.NumberFormatOptions`. It changes the display only; the committed value is
345
+ always a plain number. Two things to know:
346
+
347
+ * **Percent** (`{ style: "percent" }`) treats the value as a ratio: `0.5`
348
+ renders `50%`, and the stepper moves in ratio units. If you want the value to
349
+ be the number itself (`50` → `50%`), use `{ style: "unit", unit: "percent" }`.
350
+ * With no `format`, typed decimals are preserved and grouping is off
351
+ (`useGrouping: false`), so large numbers render without thousands separators
352
+ unless you opt in.
353
+
354
+
355
+ ## Props
356
+
357
+ ### Web
358
+
359
+ #### InputNumberExperimental
360
+
361
+ | Prop | Type | Required | Default | Description |
362
+ |------|------|----------|---------|-------------|
363
+ | `align` | `"center" | "right"` | No | — | |
364
+ | `autoComplete` | `InputNumberExperimentalAutoComplete` | No | — | |
365
+ | `className` | `string` | No | — | |
366
+ | `description` | `ReactNode` | No | — | |
367
+ | `disabled` | `boolean` | No | — | |
368
+ | `error` | `string` | No | — | Renders a styled error message below the field. |
369
+ | `format` | `NumberFormatOptions` | No | — | Number formatting for the displayed value, forwarded to Base UI `NumberField`'s native `format`. When omitted, typed ... |
370
+ | `id` | `string` | No | — | |
371
+ | `inline` | `boolean` | No | — | Shrink-wrap the field to its content (auto width). |
372
+ | `inputMode` | `"decimal" | "numeric"` | No | — | |
373
+ | `invalid` | `boolean` | No | — | Style the error border without showing an error message. |
374
+ | `label` | `string` | No | — | Floating field label. |
375
+ | `loading` | `boolean` | No | — | Shows a non-blocking loading indicator in the stepper's slot. The field stays editable (use `readOnly`/`disabled` to ... |
376
+ | `max` | `number` | No | — | |
377
+ | `min` | `number` | No | — | |
378
+ | `name` | `string` | No | — | |
379
+ | `onBlur` | `(event?: FocusEvent<HTMLInputElement, Element>) => void` | No | — | |
380
+ | `onEnter` | `(event: KeyboardEvent<HTMLInputElement>) => void` | No | — | Fires when Enter is pressed without modifier keys (Shift/Ctrl/Meta). |
381
+ | `onFocus` | `(event?: FocusEvent<HTMLInputElement, Element>) => void` | No | — | |
382
+ | `onKeyDown` | `(event: KeyboardEvent<HTMLInputElement>) => void` | No | — | |
383
+ | `onKeyUp` | `(event: KeyboardEvent<HTMLInputElement>) => void` | No | — | |
384
+ | `onValueChange` | `(newValue: number) => void` | No | — | Fires on every parsed value change (typing, paste, stepper, arrow step). Emits `null` when the field is empty. |
385
+ | `onValueCommitted` | `(newValue: number) => void` | No | — | Fires when the user commits a value (blur, Enter, stepper, arrow step). Emits `null` when committed empty. For per-ke... |
386
+ | `prefix` | `InputNumberExperimentalAffix` | No | — | |
387
+ | `readOnly` | `boolean` | No | — | |
388
+ | `ref` | `Ref<InputNumberExperimentalRef>` | No | — | |
389
+ | `showMiniLabel` | `boolean` | No | — | Default `true`. When `false`, the floating label is hidden. |
390
+ | `size` | `InputNumberExperimentalSize` | No | — | |
391
+ | `step` | `number` | No | — | Amount the stepper buttons and ArrowUp/ArrowDown keys change the value by. Default `1`. |
392
+ | `style` | `CSSProperties` | No | — | |
393
+ | `suffix` | `InputNumberExperimentalSuffixProp` | No | — | |
394
+ | `value` | `number` | No | — | Controlled value. `number` sets the value; `null` (or `undefined`) is an empty field. |
395
+
396
+ #### InputNumberExperimental.Affix
397
+
398
+ | Prop | Type | Required | Default | Description |
399
+ |------|------|----------|---------|-------------|
400
+ | `variation` | `"prefix" | "suffix"` | Yes | — | |
401
+ | `ariaLabel` | `string` | No | — | |
402
+ | `children` | `ReactNode` | No | — | Arbitrary affix content, beyond the built-in `label`/`icon`. |
403
+ | `className` | `string` | No | — | |
404
+ | `icon` | `IconNames` | No | — | |
405
+ | `label` | `string` | No | — | |
406
+ | `onClick` | `() => void` | No | — | |
407
+ | `style` | `CSSProperties` | No | — | |
408
+
409
+ #### InputNumberExperimental.Decrement
410
+
411
+ | Prop | Type | Required | Default | Description |
412
+ |------|------|----------|---------|-------------|
413
+ | `ariaLabel` | `string` | No | — | Accessible label for the button. |
414
+ | `children` | `ReactNode` | No | — | Icon content. Falls back to the default Atlantis stepper icon. |
415
+ | `className` | `string` | No | — | |
416
+ | `style` | `CSSProperties` | No | — | |
417
+
418
+ #### InputNumberExperimental.Description
419
+
420
+ | Prop | Type | Required | Default | Description |
421
+ |------|------|----------|---------|-------------|
422
+ | `className` | `string` | No | — | |
423
+ | `style` | `CSSProperties` | No | — | |
424
+
425
+ #### InputNumberExperimental.Error
426
+
427
+ | Prop | Type | Required | Default | Description |
428
+ |------|------|----------|---------|-------------|
429
+ | `className` | `string` | No | — | |
430
+ | `style` | `CSSProperties` | No | — | |
431
+
432
+ #### InputNumberExperimental.Footer
433
+
434
+ | Prop | Type | Required | Default | Description |
435
+ |------|------|----------|---------|-------------|
436
+ | `className` | `string` | No | — | |
437
+ | `style` | `CSSProperties` | No | — | |
438
+
439
+ #### InputNumberExperimental.Group
440
+
441
+ | Prop | Type | Required | Default | Description |
442
+ |------|------|----------|---------|-------------|
443
+ | `className` | `string` | No | — | |
444
+ | `style` | `CSSProperties` | No | — | |
445
+
446
+ #### InputNumberExperimental.Increment
447
+
448
+ | Prop | Type | Required | Default | Description |
449
+ |------|------|----------|---------|-------------|
450
+ | `ariaLabel` | `string` | No | — | Accessible label for the button. |
451
+ | `children` | `ReactNode` | No | — | Icon content. Falls back to the default Atlantis stepper icon. |
452
+ | `className` | `string` | No | — | |
453
+ | `style` | `CSSProperties` | No | — | |
454
+
455
+ #### InputNumberExperimental.Input
456
+
457
+ | Prop | Type | Required | Default | Description |
458
+ |------|------|----------|---------|-------------|
459
+ | `children` | `ReactNode` | No | — | Content rendered inside the input area (e.g. `.Label`, `.Stepper`). |
460
+ | `className` | `string` | No | — | |
461
+ | `style` | `CSSProperties` | No | — | |
462
+
463
+ #### InputNumberExperimental.Label
464
+
465
+ | Prop | Type | Required | Default | Description |
466
+ |------|------|----------|---------|-------------|
467
+ | `className` | `string` | No | — | |
468
+ | `style` | `CSSProperties` | No | — | |
469
+
470
+ #### InputNumberExperimental.Loading
471
+
472
+ | Prop | Type | Required | Default | Description |
473
+ |------|------|----------|---------|-------------|
474
+ | `children` | `ReactNode` | No | — | Indicator content. Falls back to the default `ActivityIndicator`. |
475
+ | `className` | `string` | No | — | |
476
+ | `style` | `CSSProperties` | No | — | |
477
+
478
+ #### InputNumberExperimental.Stepper
479
+
480
+ | Prop | Type | Required | Default | Description |
481
+ |------|------|----------|---------|-------------|
482
+ | `children` | `ReactNode` | No | — | Stepper buttons. Falls back to the default increment/decrement pair. |
483
+ | `className` | `string` | No | — | |
484
+ | `decrementLabel` | `string` | No | — | Accessible label for the decrement button. Defaults to `Decrease value`. |
485
+ | `incrementLabel` | `string` | No | — | Accessible label for the increment button. Defaults to `Increase value`. |
486
+ | `style` | `CSSProperties` | No | — | |
487
+
488
+ #### InputNumberExperimental.Wrapper
489
+
490
+ | Prop | Type | Required | Default | Description |
491
+ |------|------|----------|---------|-------------|
492
+ | `align` | `"center" | "right"` | No | — | |
493
+ | `autoComplete` | `InputNumberExperimentalAutoComplete` | No | — | |
494
+ | `children` | `ReactNode` | No | — | Composed parts (`.Group`, `.Footer`, and the parts within them). |
495
+ | `className` | `string` | No | — | |
496
+ | `disabled` | `boolean` | No | — | |
497
+ | `format` | `NumberFormatOptions` | No | — | Number formatting for the displayed value, forwarded to Base UI `NumberField`'s native `format`. When omitted, typed ... |
498
+ | `id` | `string` | No | — | |
499
+ | `inline` | `boolean` | No | — | Shrink-wrap the field to its content (auto width). |
500
+ | `inputMode` | `"decimal" | "numeric"` | No | — | |
501
+ | `invalid` | `boolean` | No | — | Style the error border without showing an error message. |
502
+ | `loading` | `boolean` | No | — | Shows a non-blocking loading indicator in the stepper's slot. The field stays editable (use `readOnly`/`disabled` to ... |
503
+ | `max` | `number` | No | — | |
504
+ | `min` | `number` | No | — | |
505
+ | `name` | `string` | No | — | |
506
+ | `onBlur` | `(event?: FocusEvent<HTMLInputElement, Element>) => void` | No | — | |
507
+ | `onEnter` | `(event: KeyboardEvent<HTMLInputElement>) => void` | No | — | Fires when Enter is pressed without modifier keys (Shift/Ctrl/Meta). |
508
+ | `onFocus` | `(event?: FocusEvent<HTMLInputElement, Element>) => void` | No | — | |
509
+ | `onKeyDown` | `(event: KeyboardEvent<HTMLInputElement>) => void` | No | — | |
510
+ | `onKeyUp` | `(event: KeyboardEvent<HTMLInputElement>) => void` | No | — | |
511
+ | `onValueChange` | `(newValue: number) => void` | No | — | Fires on every parsed value change (typing, paste, stepper, arrow step). Emits `null` when the field is empty. |
512
+ | `onValueCommitted` | `(newValue: number) => void` | No | — | Fires when the user commits a value (blur, Enter, stepper, arrow step). Emits `null` when committed empty. For per-ke... |
513
+ | `readOnly` | `boolean` | No | — | |
514
+ | `ref` | `Ref<InputNumberExperimentalRef>` | No | — | |
515
+ | `showMiniLabel` | `boolean` | No | — | Default `true`. When `false`, the floating label is hidden. |
516
+ | `size` | `InputNumberExperimentalSize` | No | — | |
517
+ | `step` | `number` | No | — | Amount the stepper buttons and ArrowUp/ArrowDown keys change the value by. Default `1`. |
518
+ | `style` | `CSSProperties` | No | — | |
519
+ | `value` | `number` | No | — | Controlled value. `number` sets the value; `null` (or `undefined`) is an empty field. |
@@ -55,6 +55,7 @@
55
55
  [InputFile](./InputFile/InputFile.md)
56
56
  [InputGroup](./InputGroup/InputGroup.md)
57
57
  [InputNumber](./InputNumber/InputNumber.md)
58
+ [InputNumberExperimental](./InputNumberExperimental/InputNumberExperimental.md)
58
59
  [InputPhoneNumber](./InputPhoneNumber/InputPhoneNumber.md)
59
60
  [InputText](./InputText/InputText.md)
60
61
  [InputTime](./InputTime/InputTime.md)
package/dist/index.cjs CHANGED
@@ -3,7 +3,6 @@
3
3
  var ActivityIndicator = require('./ActivityIndicator-cjs.js');
4
4
  var AtlantisContext = require('./AtlantisContext-cjs.js');
5
5
  var AtlantisThemeContext = require('./AtlantisThemeContext-cjs.js');
6
- var updateTheme = require('./updateTheme-cjs.js');
7
6
  var Autocomplete = require('./Autocomplete-cjs.js');
8
7
  var Avatar = require('./Avatar-cjs.js');
9
8
  var Banner = require('./Banner-cjs.js');
@@ -215,8 +214,8 @@ exports.useAtlantisContext = AtlantisContext.useAtlantisContext;
215
214
  exports.AtlantisThemeContextProvider = AtlantisThemeContext.AtlantisThemeContextProvider;
216
215
  exports.THEME_CHANGE_EVENT = AtlantisThemeContext.THEME_CHANGE_EVENT;
217
216
  exports.atlantisThemeContextDefaultValues = AtlantisThemeContext.atlantisThemeContextDefaultValues;
217
+ exports.updateTheme = AtlantisThemeContext.updateTheme;
218
218
  exports.useAtlantisTheme = AtlantisThemeContext.useAtlantisTheme;
219
- exports.updateTheme = updateTheme.updateTheme;
220
219
  exports.Autocomplete = Autocomplete.Autocomplete;
221
220
  exports.Avatar = Avatar.Avatar;
222
221
  exports.Banner = Banner.Banner;
package/dist/index.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  export { A as ActivityIndicator } from './ActivityIndicator-es.js';
2
2
  export { A as AtlantisContext, a as atlantisContextDefaultValues, u as useAtlantisContext } from './AtlantisContext-es.js';
3
- export { A as AtlantisThemeContextProvider, T as THEME_CHANGE_EVENT, a as atlantisThemeContextDefaultValues, u as useAtlantisTheme } from './AtlantisThemeContext-es.js';
4
- export { u as updateTheme } from './updateTheme-es.js';
3
+ export { A as AtlantisThemeContextProvider, T as THEME_CHANGE_EVENT, a as atlantisThemeContextDefaultValues, b as updateTheme, u as useAtlantisTheme } from './AtlantisThemeContext-es.js';
5
4
  export { A as Autocomplete } from './Autocomplete-es.js';
6
5
  export { A as Avatar } from './Avatar-es.js';
7
6
  export { B as Banner } from './Banner-es.js';
@@ -586,4 +586,4 @@ var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedA
586
586
 
587
587
  var isTypedArray_1 = isTypedArray;
588
588
 
589
- export { _isPrototype as _, isArray_1 as a, isArrayLike_1 as b, isBufferExports as c, isTypedArray_1 as d, _overArg as e, _getNative as f, _Map as g, _toSource as h, isArguments_1 as i, isFunction_1 as j, _baseUnary as k, isLength_1 as l, _nodeUtilExports as m };
589
+ export { _isPrototype as _, isArray_1 as a, isArrayLike_1 as b, isBufferExports as c, isTypedArray_1 as d, _overArg as e, _getNative as f, _Map as g, _toSource as h, isArguments_1 as i, _baseUnary as j, isFunction_1 as k, isLength_1 as l, _nodeUtilExports as m };
@@ -1,5 +1,12 @@
1
+ import type { Ref } from "react";
1
2
  import React from "react";
2
3
  import type { InputNumberExperimentalAffixCompoundProps, InputNumberExperimentalDescriptionProps, InputNumberExperimentalErrorProps, InputNumberExperimentalFooterProps, InputNumberExperimentalGroupProps, InputNumberExperimentalInputProps, InputNumberExperimentalLabelProps, InputNumberExperimentalLoadingProps, InputNumberExperimentalProps, InputNumberExperimentalRef, InputNumberExperimentalStepButtonProps, InputNumberExperimentalStepperProps, InputNumberExperimentalWrapperProps } from "./types";
4
+ declare function InputNumberExperimentalWrapper({ ref, ...props }: InputNumberExperimentalWrapperProps & {
5
+ readonly ref?: Ref<InputNumberExperimentalRef>;
6
+ }): React.JSX.Element;
7
+ declare namespace InputNumberExperimentalWrapper {
8
+ var displayName: string;
9
+ }
3
10
  declare function InputNumberExperimentalGroup({ children, className, style, }: InputNumberExperimentalGroupProps): React.JSX.Element;
4
11
  declare namespace InputNumberExperimentalGroup {
5
12
  var displayName: string;
@@ -44,18 +51,27 @@ declare function InputNumberExperimentalAffixCompound({ variation, label, icon,
44
51
  declare namespace InputNumberExperimentalAffixCompound {
45
52
  var displayName: string;
46
53
  }
47
- export declare const InputNumberExperimental: React.ForwardRefExoticComponent<InputNumberExperimentalProps & React.RefAttributes<InputNumberExperimentalRef>> & {
48
- Wrapper: React.ForwardRefExoticComponent<InputNumberExperimentalWrapperProps & React.RefAttributes<InputNumberExperimentalRef>>;
49
- Group: typeof InputNumberExperimentalGroup;
50
- Footer: typeof InputNumberExperimentalFooter;
51
- Input: typeof InputNumberExperimentalInput;
52
- Label: typeof InputNumberExperimentalLabel;
53
- Description: typeof InputNumberExperimentalDescription;
54
- Error: typeof InputNumberExperimentalError;
55
- Loading: typeof InputNumberExperimentalLoading;
56
- Stepper: typeof InputNumberExperimentalStepper;
57
- Increment: typeof InputNumberExperimentalIncrement;
58
- Decrement: typeof InputNumberExperimentalDecrement;
59
- Affix: typeof InputNumberExperimentalAffixCompound;
60
- };
61
- export type { InputNumberExperimentalAffix, InputNumberExperimentalProps, InputNumberExperimentalRef, InputNumberExperimentalSuffix, InputNumberExperimentalSuffixProp, InputNumberExperimentalWrapperProps, } from "./types";
54
+ /**
55
+ * Sugar layer: composes the parts from props. This is the same composition a
56
+ * consumer would write by hand with `<InputNumberExperimental.Wrapper>` and the
57
+ * parts — to customize a single piece, copy this tree and swap that part.
58
+ */
59
+ declare function InputNumberExperimental({ ref, ...props }: InputNumberExperimentalProps & {
60
+ readonly ref?: Ref<InputNumberExperimentalRef>;
61
+ }): React.JSX.Element;
62
+ declare namespace InputNumberExperimental {
63
+ var displayName: string;
64
+ var Wrapper: typeof InputNumberExperimentalWrapper;
65
+ var Group: typeof InputNumberExperimentalGroup;
66
+ var Footer: typeof InputNumberExperimentalFooter;
67
+ var Input: typeof InputNumberExperimentalInput;
68
+ var Label: typeof InputNumberExperimentalLabel;
69
+ var Description: typeof InputNumberExperimentalDescription;
70
+ var Error: typeof InputNumberExperimentalError;
71
+ var Loading: typeof InputNumberExperimentalLoading;
72
+ var Stepper: typeof InputNumberExperimentalStepper;
73
+ var Increment: typeof InputNumberExperimentalIncrement;
74
+ var Decrement: typeof InputNumberExperimentalDecrement;
75
+ var Affix: typeof InputNumberExperimentalAffixCompound;
76
+ }
77
+ export { InputNumberExperimental };
@@ -63,7 +63,6 @@ export interface InputNumberExperimentalWrapperProps extends InputNumberExperime
63
63
  */
64
64
  readonly loading?: boolean;
65
65
  readonly max?: number;
66
- readonly maxLength?: number;
67
66
  readonly min?: number;
68
67
  readonly name?: string;
69
68
  readonly onBlur?: (event?: FocusEvent<HTMLInputElement>) => void;
@@ -3,7 +3,7 @@ import { g as getDefaultExportFromCjs } from './_commonjsHelpers-es.js';
3
3
  import { _ as _baseFlatten } from './_baseFlatten-es.js';
4
4
  import { a as _arrayMap, b as _baseGet } from './_getAllKeys-es.js';
5
5
  import { _ as _baseEach, a as _baseIteratee } from './_baseEach-es.js';
6
- import { b as isArrayLike_1, k as _baseUnary, a as isArray_1 } from './isTypedArray-es.js';
6
+ import { b as isArrayLike_1, j as _baseUnary, a as isArray_1 } from './isTypedArray-es.js';
7
7
  import { i as isSymbol_1 } from './isSymbol-es.js';
8
8
  import { i as identity_1 } from './identity-es.js';
9
9
  import { _ as _baseRest, a as _isIterateeCall } from './_isIterateeCall-es.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components",
3
- "version": "8.25.1",
3
+ "version": "8.26.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -545,5 +545,5 @@
545
545
  "> 1%",
546
546
  "IE 10"
547
547
  ],
548
- "gitHead": "1ad8858948625e5d646ba969560948576dc5e721"
548
+ "gitHead": "4542a9c611cd00d7d25a48138bfed4365b3b0097"
549
549
  }
@@ -1,20 +0,0 @@
1
- 'use strict';
2
-
3
- var AtlantisThemeContext = require('./AtlantisThemeContext-cjs.js');
4
-
5
- /**
6
- * Toggle the theme of the application. If subscribed, the tokens will be updated accordingly.
7
- * Will add data-theme="<newTheme>" to the <html> element.
8
- * Will dispatch a custom event "atlantis.toggle-theme" with the new theme for other providers to subscribe to.
9
- */
10
- function updateTheme(theme) {
11
- if (!globalThis.window)
12
- return;
13
- const event = new CustomEvent(AtlantisThemeContext.THEME_CHANGE_EVENT, {
14
- detail: { theme },
15
- });
16
- globalThis.document.documentElement.setAttribute("data-theme", theme);
17
- globalThis.window.dispatchEvent(event);
18
- }
19
-
20
- exports.updateTheme = updateTheme;
@@ -1,18 +0,0 @@
1
- import { T as THEME_CHANGE_EVENT } from './AtlantisThemeContext-es.js';
2
-
3
- /**
4
- * Toggle the theme of the application. If subscribed, the tokens will be updated accordingly.
5
- * Will add data-theme="<newTheme>" to the <html> element.
6
- * Will dispatch a custom event "atlantis.toggle-theme" with the new theme for other providers to subscribe to.
7
- */
8
- function updateTheme(theme) {
9
- if (!globalThis.window)
10
- return;
11
- const event = new CustomEvent(THEME_CHANGE_EVENT, {
12
- detail: { theme },
13
- });
14
- globalThis.document.documentElement.setAttribute("data-theme", theme);
15
- globalThis.window.dispatchEvent(event);
16
- }
17
-
18
- export { updateTheme as u };