@jsenv/navi 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20528,6 +20528,126 @@ const InputRadioWithAction = () => {
20528
20528
  };
20529
20529
  const InputRadioInsideForm = InputRadio;
20530
20530
 
20531
+ /**
20532
+ * Merges a component's base className with className received from props.
20533
+ *
20534
+ * ```jsx
20535
+ * const MyButton = ({ className, children }) => (
20536
+ * <button className={withPropsClassName("btn", className)}>
20537
+ * {children}
20538
+ * </button>
20539
+ * );
20540
+ *
20541
+ * // Usage:
20542
+ * <MyButton className="primary large" /> // Results in "btn primary large"
20543
+ * <MyButton /> // Results in "btn"
20544
+ * ```
20545
+ *
20546
+ * @param {string} baseClassName - The component's base CSS class name
20547
+ * @param {string} [classNameFromProps] - Additional className from props (optional)
20548
+ * @returns {string} The merged className string
20549
+ */
20550
+ const withPropsClassName = (baseClassName, classNameFromProps) => {
20551
+ if (!classNameFromProps) {
20552
+ return baseClassName;
20553
+ }
20554
+
20555
+ // Trim and normalize whitespace from the props className
20556
+ const trimmedPropsClassName = classNameFromProps.trim();
20557
+ if (!trimmedPropsClassName) {
20558
+ return baseClassName;
20559
+ }
20560
+
20561
+ // Normalize multiple spaces to single spaces and combine
20562
+ const normalizedPropsClassName = trimmedPropsClassName.replace(/\s+/g, " ");
20563
+ if (!baseClassName) {
20564
+ return normalizedPropsClassName;
20565
+ }
20566
+ return `${baseClassName} ${normalizedPropsClassName}`;
20567
+ };
20568
+
20569
+ /**
20570
+ * Merges a component's base style with style received from props.
20571
+ *
20572
+ * ```jsx
20573
+ * const MyButton = ({ style, children }) => (
20574
+ * <button style={withPropsStyle({ padding: '10px' }, style)}>
20575
+ * {children}
20576
+ * </button>
20577
+ * );
20578
+ *
20579
+ * // Usage:
20580
+ * <MyButton style={{ color: 'red', fontSize: '14px' }} />
20581
+ * <MyButton style="color: blue; margin: 5px;" />
20582
+ * <MyButton /> // Just base styles
20583
+ * ```
20584
+ *
20585
+ * @param {string|object} baseStyle - The component's base style (string or object)
20586
+ * @param {string|object} [styleFromProps] - Additional style from props (optional)
20587
+ * @returns {object} The merged style object
20588
+ */
20589
+ const withPropsStyle = (baseStyle, styleFromProps) => {
20590
+ if (!styleFromProps) {
20591
+ return baseStyle;
20592
+ }
20593
+ if (!baseStyle) {
20594
+ return styleFromProps;
20595
+ }
20596
+
20597
+ // Parse base style to object if it's a string
20598
+ const parsedBaseStyle =
20599
+ typeof baseStyle === "string"
20600
+ ? parseStyleString(baseStyle)
20601
+ : baseStyle || {};
20602
+ // Parse props style to object if it's a string
20603
+ const parsedPropsStyle =
20604
+ typeof styleFromProps === "string"
20605
+ ? parseStyleString(styleFromProps)
20606
+ : styleFromProps;
20607
+ // Merge styles with props taking priority
20608
+ return { ...parsedBaseStyle, ...parsedPropsStyle };
20609
+ };
20610
+
20611
+ /**
20612
+ * Parses a CSS style string into a style object.
20613
+ * Handles CSS properties with proper camelCase conversion.
20614
+ *
20615
+ * @param {string} styleString - CSS style string like "color: red; font-size: 14px;"
20616
+ * @returns {object} Style object with camelCase properties
20617
+ */
20618
+ const parseStyleString = (styleString) => {
20619
+ const style = {};
20620
+
20621
+ if (!styleString || typeof styleString !== "string") {
20622
+ return style;
20623
+ }
20624
+
20625
+ // Split by semicolon and process each declaration
20626
+ const declarations = styleString.split(";");
20627
+
20628
+ for (let declaration of declarations) {
20629
+ declaration = declaration.trim();
20630
+ if (!declaration) continue;
20631
+
20632
+ const colonIndex = declaration.indexOf(":");
20633
+ if (colonIndex === -1) continue;
20634
+
20635
+ const property = declaration.slice(0, colonIndex).trim();
20636
+ const value = declaration.slice(colonIndex + 1).trim();
20637
+
20638
+ if (property && value) {
20639
+ // Convert kebab-case to camelCase (e.g., "font-size" -> "fontSize")
20640
+ const camelCaseProperty = property.replace(/-([a-z])/g, (match, letter) =>
20641
+ letter.toUpperCase(),
20642
+ );
20643
+
20644
+ style[camelCaseProperty] = value;
20645
+ }
20646
+ }
20647
+
20648
+ return style;
20649
+ };
20650
+
20531
20651
  installImportMetaCss(import.meta);import.meta.css = /* css */`
20532
20652
  @layer navi {
20533
20653
  .navi_input {
@@ -20641,11 +20761,13 @@ const InputTextualBasic = forwardRef((props, ref) => {
20641
20761
  autoFocus,
20642
20762
  autoFocusVisible,
20643
20763
  autoSelect,
20764
+ // visual
20644
20765
  appearance = "navi",
20645
20766
  accentColor,
20646
- style,
20647
20767
  width,
20648
20768
  height,
20769
+ className,
20770
+ style,
20649
20771
  ...rest
20650
20772
  } = props;
20651
20773
  const innerRef = useRef();
@@ -20662,19 +20784,14 @@ const InputTextualBasic = forwardRef((props, ref) => {
20662
20784
  });
20663
20785
  useConstraints(innerRef, constraints);
20664
20786
  const innerStyle = {
20665
- ...style
20787
+ width,
20788
+ height
20666
20789
  };
20667
- if (width !== undefined) {
20668
- innerStyle.width = width;
20669
- }
20670
- if (height !== undefined) {
20671
- innerStyle.height = height;
20672
- }
20673
20790
  const inputTextual = jsx("input", {
20674
20791
  ...rest,
20675
20792
  ref: innerRef,
20676
- className: appearance === "navi" ? "navi_input" : undefined,
20677
- style: innerStyle,
20793
+ className: withPropsClassName(appearance === "navi" ? "navi_input" : undefined, className),
20794
+ style: withPropsStyle(innerStyle, style),
20678
20795
  type: type,
20679
20796
  "data-value": uiState,
20680
20797
  value: innerValue,
@@ -21350,6 +21467,11 @@ const Button = forwardRef((props, ref) => {
21350
21467
  WithActionInsideForm: ButtonWithActionInsideForm
21351
21468
  });
21352
21469
  });
21470
+ const alignXMapping = {
21471
+ start: undefined,
21472
+ center: "center",
21473
+ end: "flex-end"
21474
+ };
21353
21475
  const ButtonBasic = forwardRef((props, ref) => {
21354
21476
  const contextLoading = useContext(LoadingContext);
21355
21477
  const contextLoadingElement = useContext(LoadingElementContext);
@@ -21361,10 +21483,12 @@ const ButtonBasic = forwardRef((props, ref) => {
21361
21483
  loading,
21362
21484
  constraints = [],
21363
21485
  autoFocus,
21486
+ // visual
21364
21487
  appearance = "navi",
21365
21488
  alignX = "start",
21366
- style,
21367
21489
  discrete,
21490
+ className,
21491
+ style,
21368
21492
  children,
21369
21493
  ...rest
21370
21494
  } = props;
@@ -21385,16 +21509,13 @@ const ButtonBasic = forwardRef((props, ref) => {
21385
21509
  buttonChildren = children;
21386
21510
  }
21387
21511
  const innerStyle = {
21388
- ...style
21512
+ "align-self": alignXMapping[alignX]
21389
21513
  };
21390
- if (alignX !== "start") {
21391
- innerStyle["align-self"] = alignX === "center" ? "center" : "flex-end";
21392
- }
21393
21514
  return jsx("button", {
21394
21515
  ...rest,
21395
21516
  ref: innerRef,
21396
- className: appearance === "navi" ? "navi_button" : undefined,
21397
- style: innerStyle,
21517
+ className: withPropsClassName(appearance === "navi" ? "navi_button" : undefined, className),
21518
+ style: withPropsStyle(innerStyle, style),
21398
21519
  disabled: innerDisabled,
21399
21520
  "data-discrete": discrete ? "" : undefined,
21400
21521
  "data-readonly": innerReadOnly ? "" : undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Library of components including navigation to create frontend applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,6 +15,8 @@ import { renderActionableComponent } from "../action_execution/render_actionable
15
15
  import { useAction } from "../action_execution/use_action.js";
16
16
  import { useExecuteAction } from "../action_execution/use_execute_action.js";
17
17
  import { LoaderBackground } from "../loader/loader_background.jsx";
18
+ import { withPropsClassName } from "../props_composition/with_props_class_name.js";
19
+ import { withPropsStyle } from "../props_composition/with_props_style.js";
18
20
  import { useAutoFocus } from "../use_auto_focus.js";
19
21
  import { initCustomField } from "./custom_field.js";
20
22
  import { useActionEvents } from "./use_action_events.js";
@@ -195,6 +197,11 @@ export const Button = forwardRef((props, ref) => {
195
197
  });
196
198
  });
197
199
 
200
+ const alignXMapping = {
201
+ start: undefined,
202
+ center: "center",
203
+ end: "flex-end",
204
+ };
198
205
  const ButtonBasic = forwardRef((props, ref) => {
199
206
  const contextLoading = useContext(LoadingContext);
200
207
  const contextLoadingElement = useContext(LoadingElementContext);
@@ -206,10 +213,14 @@ const ButtonBasic = forwardRef((props, ref) => {
206
213
  loading,
207
214
  constraints = [],
208
215
  autoFocus,
216
+
217
+ // visual
209
218
  appearance = "navi",
210
219
  alignX = "start",
211
- style,
212
220
  discrete,
221
+ className,
222
+ style,
223
+
213
224
  children,
214
225
  ...rest
215
226
  } = props;
@@ -230,17 +241,19 @@ const ButtonBasic = forwardRef((props, ref) => {
230
241
  buttonChildren = children;
231
242
  }
232
243
 
233
- const innerStyle = { ...style };
234
- if (alignX !== "start") {
235
- innerStyle["align-self"] = alignX === "center" ? "center" : "flex-end";
236
- }
244
+ const innerStyle = {
245
+ "align-self": alignXMapping[alignX],
246
+ };
237
247
 
238
248
  return (
239
249
  <button
240
250
  {...rest}
241
251
  ref={innerRef}
242
- className={appearance === "navi" ? "navi_button" : undefined}
243
- style={innerStyle}
252
+ className={withPropsClassName(
253
+ appearance === "navi" ? "navi_button" : undefined,
254
+ className,
255
+ )}
256
+ style={withPropsStyle(innerStyle, style)}
244
257
  disabled={innerDisabled}
245
258
  data-discrete={discrete ? "" : undefined}
246
259
  data-readonly={innerReadOnly ? "" : undefined}
@@ -32,6 +32,8 @@ import { renderActionableComponent } from "../action_execution/render_actionable
32
32
  import { useActionBoundToOneParam } from "../action_execution/use_action.js";
33
33
  import { useExecuteAction } from "../action_execution/use_execute_action.js";
34
34
  import { LoadableInlineElement } from "../loader/loader_background.jsx";
35
+ import { withPropsClassName } from "../props_composition/with_props_class_name.js";
36
+ import { withPropsStyle } from "../props_composition/with_props_style.js";
35
37
  import { useAutoFocus } from "../use_auto_focus.js";
36
38
  import { initCustomField } from "./custom_field.js";
37
39
  import { ReportReadOnlyOnLabelContext } from "./label.jsx";
@@ -163,11 +165,15 @@ const InputTextualBasic = forwardRef((props, ref) => {
163
165
  autoFocus,
164
166
  autoFocusVisible,
165
167
  autoSelect,
168
+
169
+ // visual
166
170
  appearance = "navi",
167
171
  accentColor,
168
- style,
169
172
  width,
170
173
  height,
174
+ className,
175
+ style,
176
+
171
177
  ...rest
172
178
  } = props;
173
179
  const innerRef = useRef();
@@ -188,19 +194,19 @@ const InputTextualBasic = forwardRef((props, ref) => {
188
194
  });
189
195
  useConstraints(innerRef, constraints);
190
196
 
191
- const innerStyle = { ...style };
192
- if (width !== undefined) {
193
- innerStyle.width = width;
194
- }
195
- if (height !== undefined) {
196
- innerStyle.height = height;
197
- }
197
+ const innerStyle = {
198
+ width,
199
+ height,
200
+ };
198
201
  const inputTextual = (
199
202
  <input
200
203
  {...rest}
201
204
  ref={innerRef}
202
- className={appearance === "navi" ? "navi_input" : undefined}
203
- style={innerStyle}
205
+ className={withPropsClassName(
206
+ appearance === "navi" ? "navi_input" : undefined,
207
+ className,
208
+ )}
209
+ style={withPropsStyle(innerStyle, style)}
204
210
  type={type}
205
211
  data-value={uiState}
206
212
  value={innerValue}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Merges a component's base className with className received from props.
3
+ *
4
+ * ```jsx
5
+ * const MyButton = ({ className, children }) => (
6
+ * <button className={withPropsClassName("btn", className)}>
7
+ * {children}
8
+ * </button>
9
+ * );
10
+ *
11
+ * // Usage:
12
+ * <MyButton className="primary large" /> // Results in "btn primary large"
13
+ * <MyButton /> // Results in "btn"
14
+ * ```
15
+ *
16
+ * @param {string} baseClassName - The component's base CSS class name
17
+ * @param {string} [classNameFromProps] - Additional className from props (optional)
18
+ * @returns {string} The merged className string
19
+ */
20
+ export const withPropsClassName = (baseClassName, classNameFromProps) => {
21
+ if (!classNameFromProps) {
22
+ return baseClassName;
23
+ }
24
+
25
+ // Trim and normalize whitespace from the props className
26
+ const trimmedPropsClassName = classNameFromProps.trim();
27
+ if (!trimmedPropsClassName) {
28
+ return baseClassName;
29
+ }
30
+
31
+ // Normalize multiple spaces to single spaces and combine
32
+ const normalizedPropsClassName = trimmedPropsClassName.replace(/\s+/g, " ");
33
+ if (!baseClassName) {
34
+ return normalizedPropsClassName;
35
+ }
36
+ return `${baseClassName} ${normalizedPropsClassName}`;
37
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Merges a component's base style with style received from props.
3
+ *
4
+ * ```jsx
5
+ * const MyButton = ({ style, children }) => (
6
+ * <button style={withPropsStyle({ padding: '10px' }, style)}>
7
+ * {children}
8
+ * </button>
9
+ * );
10
+ *
11
+ * // Usage:
12
+ * <MyButton style={{ color: 'red', fontSize: '14px' }} />
13
+ * <MyButton style="color: blue; margin: 5px;" />
14
+ * <MyButton /> // Just base styles
15
+ * ```
16
+ *
17
+ * @param {string|object} baseStyle - The component's base style (string or object)
18
+ * @param {string|object} [styleFromProps] - Additional style from props (optional)
19
+ * @returns {object} The merged style object
20
+ */
21
+ export const withPropsStyle = (baseStyle, styleFromProps) => {
22
+ if (!styleFromProps) {
23
+ return baseStyle;
24
+ }
25
+ if (!baseStyle) {
26
+ return styleFromProps;
27
+ }
28
+
29
+ // Parse base style to object if it's a string
30
+ const parsedBaseStyle =
31
+ typeof baseStyle === "string"
32
+ ? parseStyleString(baseStyle)
33
+ : baseStyle || {};
34
+ // Parse props style to object if it's a string
35
+ const parsedPropsStyle =
36
+ typeof styleFromProps === "string"
37
+ ? parseStyleString(styleFromProps)
38
+ : styleFromProps;
39
+ // Merge styles with props taking priority
40
+ return { ...parsedBaseStyle, ...parsedPropsStyle };
41
+ };
42
+
43
+ /**
44
+ * Parses a CSS style string into a style object.
45
+ * Handles CSS properties with proper camelCase conversion.
46
+ *
47
+ * @param {string} styleString - CSS style string like "color: red; font-size: 14px;"
48
+ * @returns {object} Style object with camelCase properties
49
+ */
50
+ const parseStyleString = (styleString) => {
51
+ const style = {};
52
+
53
+ if (!styleString || typeof styleString !== "string") {
54
+ return style;
55
+ }
56
+
57
+ // Split by semicolon and process each declaration
58
+ const declarations = styleString.split(";");
59
+
60
+ for (let declaration of declarations) {
61
+ declaration = declaration.trim();
62
+ if (!declaration) continue;
63
+
64
+ const colonIndex = declaration.indexOf(":");
65
+ if (colonIndex === -1) continue;
66
+
67
+ const property = declaration.slice(0, colonIndex).trim();
68
+ const value = declaration.slice(colonIndex + 1).trim();
69
+
70
+ if (property && value) {
71
+ // Convert kebab-case to camelCase (e.g., "font-size" -> "fontSize")
72
+ const camelCaseProperty = property.replace(/-([a-z])/g, (match, letter) =>
73
+ letter.toUpperCase(),
74
+ );
75
+
76
+ style[camelCaseProperty] = value;
77
+ }
78
+ }
79
+
80
+ return style;
81
+ };