@neumorphism-ui/react 1.0.0-next.1 → 1.0.0-next.2

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
@@ -2,7 +2,8 @@
2
2
 
3
3
  React 18.3 and 19 wrappers for [neumorphism-ui](https://github.com/hunghdvn/neumorphism-ui):
4
4
  `NeuButton`, `NeuSwitch`, `NeuTextField`, `NeuIcon`, `NeuIconButton`, `NeuButtonGroup`,
5
- `NeuSurface`, `NeuDivider`, `NeuBadge`, `NeuAvatar` and `NeuSpinner`, generated from the element definitions. They set props
5
+ `NeuSurface`, `NeuDivider`, `NeuBadge`, `NeuAvatar`, `NeuSpinner`, `NeuTextarea`, `NeuNumberField`,
6
+ `NeuCheckbox`, `NeuRadioGroup`, `NeuRadio`, `NeuSlider`, `NeuSelect` and `NeuOption`, generated from the element definitions. They set props
6
7
  as element properties, forward refs to the real element, work controlled or uncontrolled like
7
8
  React's `<input>`, and server-render declarative shadow roots, so they work with Next.js App Router
8
9
  streaming. Pre-release.
@@ -19,6 +20,19 @@ import { NeuSwitch, NeuTextField } from '@neumorphism-ui/react';
19
20
  <NeuSwitch name="notify" defaultChecked>Notifications</NeuSwitch>;
20
21
  ```
21
22
 
23
+ ```tsx
24
+ import { NeuOption, NeuRadio, NeuRadioGroup, NeuSelect } from '@neumorphism-ui/react';
25
+
26
+ <NeuSelect label="Plan" name="plan" defaultValue="pro">
27
+ <NeuOption value="free">Free</NeuOption>
28
+ <NeuOption value="pro">Pro</NeuOption>
29
+ </NeuSelect>;
30
+ <NeuRadioGroup label="Billing" name="billing" defaultValue="monthly">
31
+ <NeuRadio value="monthly">Monthly</NeuRadio>
32
+ <NeuRadio value="yearly">Yearly</NeuRadio>
33
+ </NeuRadioGroup>;
34
+ ```
35
+
22
36
  ## Icon libraries
23
37
 
24
38
  `NeuIcon` and `NeuIconButton` ship the built-in `system` icons. Register your own library with
@@ -1,15 +1,58 @@
1
1
  'use client';
2
- import { attributeName, coerceProperty, formDefaultProp, renderShadowContents, serializeAttribute, } from '@neumorphism-ui/elements/ssr.js';
2
+ import { attributeName, coerceProperty, formDefaultProp, optionLabel, renderShadowContents, serializeAttribute, } from '@neumorphism-ui/elements/ssr.js';
3
3
  import * as React from 'react';
4
4
  const isServer = typeof window === 'undefined';
5
5
  const useIsomorphicLayoutEffect = isServer ? React.useEffect : React.useLayoutEffect;
6
6
  const needsClassAttribute = Number.parseInt(React.version, 10) < 19;
7
+ function textOf(node) {
8
+ if (typeof node === 'string' || typeof node === 'number')
9
+ return String(node);
10
+ if (Array.isArray(node))
11
+ return node.map(textOf).join('');
12
+ if (React.isValidElement(node))
13
+ return textOf(node.props.children);
14
+ return '';
15
+ }
16
+ /**
17
+ * Option elements among the children at any depth, as the client reads them, looking through
18
+ * fragments, arrays and plain elements (spec §8.4). A component other than the option wrapper may
19
+ * render options the server cannot see, so meeting one returns no options at all: the select then
20
+ * leaves its display empty for the client to fill rather than guessing from part of the list.
21
+ */
22
+ function collectOptions(children, optionTag) {
23
+ const options = [];
24
+ let complete = true;
25
+ const walk = (node) => {
26
+ React.Children.forEach(node, (child) => {
27
+ if (!React.isValidElement(child))
28
+ return;
29
+ const props = child.props;
30
+ const type = child.type;
31
+ const isOption = type === optionTag || (typeof type !== 'string' && type?.displayName === optionTag);
32
+ if (!isOption) {
33
+ if (child.type === React.Fragment || typeof type === 'string')
34
+ walk(props.children);
35
+ else
36
+ complete = false;
37
+ return;
38
+ }
39
+ const label = optionLabel(textOf(props.children));
40
+ options.push({
41
+ value: props.value === undefined || props.value === null ? label : String(props.value),
42
+ label,
43
+ disabled: !!props.disabled,
44
+ });
45
+ });
46
+ };
47
+ walk(children);
48
+ return complete ? options : [];
49
+ }
7
50
  export function createComponent(elementClass, config = {}) {
8
51
  const def = elementClass.def;
9
52
  const events = config.events ?? {};
10
53
  const valueProp = def.form?.valueProp;
11
54
  const defaultProp = formDefaultProp(def);
12
- const elementKeys = new Set(Object.keys(def.props));
55
+ const elementKeys = new Set([...Object.keys(def.props), ...Object.keys(def.properties ?? {})]);
13
56
  if (defaultProp)
14
57
  elementKeys.add(defaultProp);
15
58
  const Wrapper = React.forwardRef(function NeuWrapper(props, forwardedRef) {
@@ -44,7 +87,9 @@ export function createComponent(elementClass, config = {}) {
44
87
  const handlersRef = React.useRef(handlers);
45
88
  handlersRef.current = handlers;
46
89
  const applied = React.useRef(new Set());
47
- // Properties after every commit. Defaults go first so a controlled value set in the same pass wins.
90
+ // Properties after every commit. Defaults go first so a controlled value set in the same pass
91
+ // wins, and the value goes last so min, max and step are in place when it is clamped (as
92
+ // ReactDOM orders a native range input).
48
93
  useIsomorphicLayoutEffect(() => {
49
94
  const node = elementRef.current;
50
95
  if (!node)
@@ -62,7 +107,8 @@ export function createComponent(elementClass, config = {}) {
62
107
  }
63
108
  }
64
109
  const next = new Set();
65
- const keys = Object.keys(element).sort((a, b) => Number(b === defaultProp) - Number(a === defaultProp));
110
+ const rank = (key) => (key === defaultProp ? 0 : key === valueProp ? 2 : 1);
111
+ const keys = Object.keys(element).sort((a, b) => rank(a) - rank(b));
66
112
  for (const key of keys) {
67
113
  const value = element[key];
68
114
  if (absent(key))
@@ -128,7 +174,13 @@ export function createComponent(elementClass, config = {}) {
128
174
  }
129
175
  const templateProps = {
130
176
  shadowrootmode: 'open',
131
- dangerouslySetInnerHTML: { __html: renderShadowContents(def, serverAttributes) },
177
+ dangerouslySetInnerHTML: {
178
+ __html: def.optionsFrom
179
+ ? renderShadowContents(def, serverAttributes, {
180
+ options: collectOptions(children, def.optionsFrom),
181
+ })
182
+ : renderShadowContents(def, serverAttributes),
183
+ },
132
184
  };
133
185
  if (def.delegatesFocus)
134
186
  templateProps.shadowrootdelegatesfocus = '';
@@ -0,0 +1,19 @@
1
+ import { NeuCheckbox as NeuCheckboxElement } from '@neumorphism-ui/elements/checkbox.js';
2
+ import { type WrapperProps } from '../create-component.js';
3
+ interface NeuCheckboxOwnProps {
4
+ checked?: boolean;
5
+ indeterminate?: boolean;
6
+ name?: string;
7
+ value?: string;
8
+ disabled?: boolean;
9
+ required?: boolean;
10
+ size?: 'sm' | 'md' | 'lg';
11
+ defaultChecked?: boolean;
12
+ onInput?: (event: Event) => void;
13
+ onChange?: (event: Event) => void;
14
+ }
15
+ export type NeuCheckboxProps = WrapperProps<keyof NeuCheckboxOwnProps> & NeuCheckboxOwnProps;
16
+ export declare const NeuCheckbox: import("react").ForwardRefExoticComponent<Omit<import("react").HTMLAttributes<HTMLElement>, keyof NeuCheckboxOwnProps> & {
17
+ children?: import("react").ReactNode;
18
+ } & NeuCheckboxOwnProps & import("react").RefAttributes<NeuCheckboxElement>>;
19
+ export {};
@@ -0,0 +1,7 @@
1
+ // Generated by tools/gen from the neu-checkbox definition. Do not edit.
2
+ 'use client';
3
+ import { NeuCheckbox as NeuCheckboxElement } from '@neumorphism-ui/elements/checkbox.js';
4
+ import { createComponent } from '../create-component.js';
5
+ export const NeuCheckbox = createComponent(NeuCheckboxElement, {
6
+ events: { onInput: 'input', onChange: 'change' },
7
+ });
@@ -2,10 +2,18 @@ export { NeuAvatar, type NeuAvatarProps } from './avatar.js';
2
2
  export { NeuBadge, type NeuBadgeProps } from './badge.js';
3
3
  export { NeuButton, type NeuButtonProps } from './button.js';
4
4
  export { NeuButtonGroup, type NeuButtonGroupProps } from './button-group.js';
5
+ export { NeuCheckbox, type NeuCheckboxProps } from './checkbox.js';
5
6
  export { NeuDivider, type NeuDividerProps } from './divider.js';
6
7
  export { NeuIcon, type NeuIconProps } from './icon.js';
7
8
  export { NeuIconButton, type NeuIconButtonProps } from './icon-button.js';
9
+ export { NeuNumberField, type NeuNumberFieldProps } from './number-field.js';
10
+ export { NeuOption, type NeuOptionProps } from './option.js';
11
+ export { NeuRadio, type NeuRadioProps } from './radio.js';
12
+ export { NeuRadioGroup, type NeuRadioGroupProps } from './radio-group.js';
13
+ export { NeuSelect, type NeuSelectProps } from './select.js';
14
+ export { NeuSlider, type NeuSliderProps } from './slider.js';
8
15
  export { NeuSpinner, type NeuSpinnerProps } from './spinner.js';
9
16
  export { NeuSurface, type NeuSurfaceProps } from './surface.js';
10
17
  export { NeuSwitch, type NeuSwitchProps } from './switch.js';
11
18
  export { NeuTextField, type NeuTextFieldProps } from './text-field.js';
19
+ export { NeuTextarea, type NeuTextareaProps } from './textarea.js';
@@ -3,10 +3,18 @@ export { NeuAvatar } from './avatar.js';
3
3
  export { NeuBadge } from './badge.js';
4
4
  export { NeuButton } from './button.js';
5
5
  export { NeuButtonGroup } from './button-group.js';
6
+ export { NeuCheckbox } from './checkbox.js';
6
7
  export { NeuDivider } from './divider.js';
7
8
  export { NeuIcon } from './icon.js';
8
9
  export { NeuIconButton } from './icon-button.js';
10
+ export { NeuNumberField } from './number-field.js';
11
+ export { NeuOption } from './option.js';
12
+ export { NeuRadio } from './radio.js';
13
+ export { NeuRadioGroup } from './radio-group.js';
14
+ export { NeuSelect } from './select.js';
15
+ export { NeuSlider } from './slider.js';
9
16
  export { NeuSpinner } from './spinner.js';
10
17
  export { NeuSurface } from './surface.js';
11
18
  export { NeuSwitch } from './switch.js';
12
19
  export { NeuTextField } from './text-field.js';
20
+ export { NeuTextarea } from './textarea.js';
@@ -0,0 +1,27 @@
1
+ import { NeuNumberField as NeuNumberFieldElement } from '@neumorphism-ui/elements/number-field.js';
2
+ import { type WrapperProps } from '../create-component.js';
3
+ interface NeuNumberFieldOwnProps {
4
+ value?: string;
5
+ min?: number;
6
+ max?: number;
7
+ step?: number;
8
+ name?: string;
9
+ label?: string;
10
+ placeholder?: string;
11
+ helpText?: string;
12
+ errorText?: string;
13
+ required?: boolean;
14
+ disabled?: boolean;
15
+ readOnly?: boolean;
16
+ size?: 'sm' | 'md' | 'lg';
17
+ incrementLabel?: string;
18
+ decrementLabel?: string;
19
+ defaultValue?: string;
20
+ onInput?: (event: Event) => void;
21
+ onChange?: (event: Event) => void;
22
+ }
23
+ export type NeuNumberFieldProps = WrapperProps<keyof NeuNumberFieldOwnProps> & NeuNumberFieldOwnProps;
24
+ export declare const NeuNumberField: import("react").ForwardRefExoticComponent<Omit<import("react").HTMLAttributes<HTMLElement>, keyof NeuNumberFieldOwnProps> & {
25
+ children?: import("react").ReactNode;
26
+ } & NeuNumberFieldOwnProps & import("react").RefAttributes<NeuNumberFieldElement>>;
27
+ export {};
@@ -0,0 +1,7 @@
1
+ // Generated by tools/gen from the neu-number-field definition. Do not edit.
2
+ 'use client';
3
+ import { NeuNumberField as NeuNumberFieldElement } from '@neumorphism-ui/elements/number-field.js';
4
+ import { createComponent } from '../create-component.js';
5
+ export const NeuNumberField = createComponent(NeuNumberFieldElement, {
6
+ events: { onInput: 'input', onChange: 'input' },
7
+ });
@@ -0,0 +1,11 @@
1
+ import { NeuOption as NeuOptionElement } from '@neumorphism-ui/elements/option.js';
2
+ import { type WrapperProps } from '../create-component.js';
3
+ interface NeuOptionOwnProps {
4
+ value?: string;
5
+ disabled?: boolean;
6
+ }
7
+ export type NeuOptionProps = WrapperProps<keyof NeuOptionOwnProps> & NeuOptionOwnProps;
8
+ export declare const NeuOption: import("react").ForwardRefExoticComponent<Omit<import("react").HTMLAttributes<HTMLElement>, keyof NeuOptionOwnProps> & {
9
+ children?: import("react").ReactNode;
10
+ } & NeuOptionOwnProps & import("react").RefAttributes<NeuOptionElement>>;
11
+ export {};
@@ -0,0 +1,7 @@
1
+ // Generated by tools/gen from the neu-option definition. Do not edit.
2
+ 'use client';
3
+ import { NeuOption as NeuOptionElement } from '@neumorphism-ui/elements/option.js';
4
+ import { createComponent } from '../create-component.js';
5
+ export const NeuOption = createComponent(NeuOptionElement, {
6
+ events: {},
7
+ });
@@ -0,0 +1,18 @@
1
+ import { NeuRadioGroup as NeuRadioGroupElement } from '@neumorphism-ui/elements/radio-group.js';
2
+ import { type WrapperProps } from '../create-component.js';
3
+ interface NeuRadioGroupOwnProps {
4
+ label?: string;
5
+ name?: string;
6
+ value?: string;
7
+ required?: boolean;
8
+ disabled?: boolean;
9
+ orientation?: 'vertical' | 'horizontal';
10
+ defaultValue?: string;
11
+ onInput?: (event: Event) => void;
12
+ onChange?: (event: Event) => void;
13
+ }
14
+ export type NeuRadioGroupProps = WrapperProps<keyof NeuRadioGroupOwnProps> & NeuRadioGroupOwnProps;
15
+ export declare const NeuRadioGroup: import("react").ForwardRefExoticComponent<Omit<import("react").HTMLAttributes<HTMLElement>, keyof NeuRadioGroupOwnProps> & {
16
+ children?: import("react").ReactNode;
17
+ } & NeuRadioGroupOwnProps & import("react").RefAttributes<NeuRadioGroupElement>>;
18
+ export {};
@@ -0,0 +1,7 @@
1
+ // Generated by tools/gen from the neu-radio-group definition. Do not edit.
2
+ 'use client';
3
+ import { NeuRadioGroup as NeuRadioGroupElement } from '@neumorphism-ui/elements/radio-group.js';
4
+ import { createComponent } from '../create-component.js';
5
+ export const NeuRadioGroup = createComponent(NeuRadioGroupElement, {
6
+ events: { onInput: 'input', onChange: 'input' },
7
+ });
@@ -0,0 +1,11 @@
1
+ import { NeuRadio as NeuRadioElement } from '@neumorphism-ui/elements/radio.js';
2
+ import { type WrapperProps } from '../create-component.js';
3
+ interface NeuRadioOwnProps {
4
+ value?: string;
5
+ disabled?: boolean;
6
+ }
7
+ export type NeuRadioProps = WrapperProps<keyof NeuRadioOwnProps> & NeuRadioOwnProps;
8
+ export declare const NeuRadio: import("react").ForwardRefExoticComponent<Omit<import("react").HTMLAttributes<HTMLElement>, keyof NeuRadioOwnProps> & {
9
+ children?: import("react").ReactNode;
10
+ } & NeuRadioOwnProps & import("react").RefAttributes<NeuRadioElement>>;
11
+ export {};
@@ -0,0 +1,7 @@
1
+ // Generated by tools/gen from the neu-radio definition. Do not edit.
2
+ 'use client';
3
+ import { NeuRadio as NeuRadioElement } from '@neumorphism-ui/elements/radio.js';
4
+ import { createComponent } from '../create-component.js';
5
+ export const NeuRadio = createComponent(NeuRadioElement, {
6
+ events: {},
7
+ });
@@ -0,0 +1,23 @@
1
+ import { NeuSelect as NeuSelectElement } from '@neumorphism-ui/elements/select.js';
2
+ import { type WrapperProps } from '../create-component.js';
3
+ interface NeuSelectOwnProps {
4
+ value?: string;
5
+ name?: string;
6
+ label?: string;
7
+ placeholder?: string;
8
+ helpText?: string;
9
+ errorText?: string;
10
+ required?: boolean;
11
+ disabled?: boolean;
12
+ size?: 'sm' | 'md' | 'lg';
13
+ defaultValue?: string;
14
+ onInput?: (event: Event) => void;
15
+ onChange?: (event: Event) => void;
16
+ onNeuOpen?: (event: Event) => void;
17
+ onNeuClose?: (event: Event) => void;
18
+ }
19
+ export type NeuSelectProps = WrapperProps<keyof NeuSelectOwnProps> & NeuSelectOwnProps;
20
+ export declare const NeuSelect: import("react").ForwardRefExoticComponent<Omit<import("react").HTMLAttributes<HTMLElement>, keyof NeuSelectOwnProps> & {
21
+ children?: import("react").ReactNode;
22
+ } & NeuSelectOwnProps & import("react").RefAttributes<NeuSelectElement>>;
23
+ export {};
@@ -0,0 +1,7 @@
1
+ // Generated by tools/gen from the neu-select definition. Do not edit.
2
+ 'use client';
3
+ import { NeuSelect as NeuSelectElement } from '@neumorphism-ui/elements/select.js';
4
+ import { createComponent } from '../create-component.js';
5
+ export const NeuSelect = createComponent(NeuSelectElement, {
6
+ events: { onInput: 'input', onChange: 'input', onNeuOpen: 'neu-open', onNeuClose: 'neu-close' },
7
+ });
@@ -0,0 +1,20 @@
1
+ import { NeuSlider as NeuSliderElement } from '@neumorphism-ui/elements/slider.js';
2
+ import { type WrapperProps } from '../create-component.js';
3
+ interface NeuSliderOwnProps {
4
+ value?: number;
5
+ min?: number;
6
+ max?: number;
7
+ step?: number;
8
+ name?: string;
9
+ label?: string;
10
+ disabled?: boolean;
11
+ defaultValue?: number;
12
+ valueFormatter?: (value: number) => string;
13
+ onInput?: (event: Event) => void;
14
+ onChange?: (event: Event) => void;
15
+ }
16
+ export type NeuSliderProps = WrapperProps<keyof NeuSliderOwnProps> & NeuSliderOwnProps;
17
+ export declare const NeuSlider: import("react").ForwardRefExoticComponent<Omit<import("react").HTMLAttributes<HTMLElement>, keyof NeuSliderOwnProps> & {
18
+ children?: import("react").ReactNode;
19
+ } & NeuSliderOwnProps & import("react").RefAttributes<NeuSliderElement>>;
20
+ export {};
@@ -0,0 +1,7 @@
1
+ // Generated by tools/gen from the neu-slider definition. Do not edit.
2
+ 'use client';
3
+ import { NeuSlider as NeuSliderElement } from '@neumorphism-ui/elements/slider.js';
4
+ import { createComponent } from '../create-component.js';
5
+ export const NeuSlider = createComponent(NeuSliderElement, {
6
+ events: { onInput: 'input', onChange: 'input' },
7
+ });
@@ -0,0 +1,28 @@
1
+ import { NeuTextarea as NeuTextareaElement } from '@neumorphism-ui/elements/textarea.js';
2
+ import { type WrapperProps } from '../create-component.js';
3
+ interface NeuTextareaOwnProps {
4
+ value?: string;
5
+ name?: string;
6
+ label?: string;
7
+ placeholder?: string;
8
+ helpText?: string;
9
+ errorText?: string;
10
+ required?: boolean;
11
+ disabled?: boolean;
12
+ readOnly?: boolean;
13
+ minLength?: number;
14
+ maxLength?: number;
15
+ autocomplete?: string;
16
+ enterKeyHint?: string;
17
+ rows?: number;
18
+ resize?: 'none' | 'vertical' | 'auto';
19
+ size?: 'sm' | 'md' | 'lg';
20
+ defaultValue?: string;
21
+ onInput?: (event: Event) => void;
22
+ onChange?: (event: Event) => void;
23
+ }
24
+ export type NeuTextareaProps = WrapperProps<keyof NeuTextareaOwnProps> & NeuTextareaOwnProps;
25
+ export declare const NeuTextarea: import("react").ForwardRefExoticComponent<Omit<import("react").HTMLAttributes<HTMLElement>, keyof NeuTextareaOwnProps> & {
26
+ children?: import("react").ReactNode;
27
+ } & NeuTextareaOwnProps & import("react").RefAttributes<NeuTextareaElement>>;
28
+ export {};
@@ -0,0 +1,7 @@
1
+ // Generated by tools/gen from the neu-textarea definition. Do not edit.
2
+ 'use client';
3
+ import { NeuTextarea as NeuTextareaElement } from '@neumorphism-ui/elements/textarea.js';
4
+ import { createComponent } from '../create-component.js';
5
+ export const NeuTextarea = createComponent(NeuTextareaElement, {
6
+ events: { onInput: 'input', onChange: 'input' },
7
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neumorphism-ui/react",
3
- "version": "1.0.0-next.1",
3
+ "version": "1.0.0-next.2",
4
4
  "description": "React wrappers for neumorphism-ui",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,7 +25,7 @@
25
25
  "provenance": true
26
26
  },
27
27
  "dependencies": {
28
- "@neumorphism-ui/elements": "1.0.0-next.1"
28
+ "@neumorphism-ui/elements": "1.0.0-next.2"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "react": "^18.3 || ^19",