@jsenv/navi 0.4.2 → 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,
@@ -21196,11 +21313,15 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
21196
21313
  @layer navi {
21197
21314
  .navi_button {
21198
21315
  position: relative;
21199
- display: inline-block;
21316
+ display: inline-flex;
21317
+ width: fit-content;
21318
+ height: fit-content;
21200
21319
  padding: 0;
21201
21320
  background: none;
21202
21321
  border: none;
21322
+ border-radius: inherit;
21203
21323
  outline: none;
21324
+ cursor: pointer;
21204
21325
 
21205
21326
  --border-width: 1px;
21206
21327
  --outline-width: 1px;
@@ -21294,6 +21415,9 @@ installImportMetaCss(import.meta);import.meta.css = /* css */`
21294
21415
  --color: var(--color-readonly);
21295
21416
  }
21296
21417
  /* Disabled */
21418
+ .navi_button[data-disabled] {
21419
+ cursor: default;
21420
+ }
21297
21421
  .navi_button[data-disabled] .navi_button_content {
21298
21422
  --border-color: var(--border-color-disabled);
21299
21423
  --background-color: var(--background-color-disabled);
@@ -21343,6 +21467,11 @@ const Button = forwardRef((props, ref) => {
21343
21467
  WithActionInsideForm: ButtonWithActionInsideForm
21344
21468
  });
21345
21469
  });
21470
+ const alignXMapping = {
21471
+ start: undefined,
21472
+ center: "center",
21473
+ end: "flex-end"
21474
+ };
21346
21475
  const ButtonBasic = forwardRef((props, ref) => {
21347
21476
  const contextLoading = useContext(LoadingContext);
21348
21477
  const contextLoadingElement = useContext(LoadingElementContext);
@@ -21354,8 +21483,12 @@ const ButtonBasic = forwardRef((props, ref) => {
21354
21483
  loading,
21355
21484
  constraints = [],
21356
21485
  autoFocus,
21486
+ // visual
21357
21487
  appearance = "navi",
21488
+ alignX = "start",
21358
21489
  discrete,
21490
+ className,
21491
+ style,
21359
21492
  children,
21360
21493
  ...rest
21361
21494
  } = props;
@@ -21375,10 +21508,15 @@ const ButtonBasic = forwardRef((props, ref) => {
21375
21508
  } else {
21376
21509
  buttonChildren = children;
21377
21510
  }
21511
+ const innerStyle = {
21512
+ "align-self": alignXMapping[alignX]
21513
+ };
21378
21514
  return jsx("button", {
21379
21515
  ...rest,
21380
21516
  ref: innerRef,
21381
- className: appearance === "navi" ? "navi_button" : undefined,
21517
+ className: withPropsClassName(appearance === "navi" ? "navi_button" : undefined, className),
21518
+ style: withPropsStyle(innerStyle, style),
21519
+ disabled: innerDisabled,
21382
21520
  "data-discrete": discrete ? "" : undefined,
21383
21521
  "data-readonly": innerReadOnly ? "" : undefined,
21384
21522
  "data-readonly-silent": innerLoading ? "" : undefined,
@@ -27636,12 +27774,14 @@ const useSignalSync = (value, initialValue = value) => {
27636
27774
  const FontSizedSvg = ({
27637
27775
  width = "1em",
27638
27776
  height = "1em",
27777
+ style,
27639
27778
  children,
27640
27779
  ...props
27641
27780
  }) => {
27642
27781
  return jsx("span", {
27643
27782
  ...props,
27644
27783
  style: {
27784
+ ...style,
27645
27785
  display: "flex",
27646
27786
  alignItems: "center",
27647
27787
  width,
@@ -27803,6 +27943,63 @@ const Overflow = ({
27803
27943
  });
27804
27944
  };
27805
27945
 
27946
+ installImportMetaCss(import.meta);import.meta.css = /* css */`
27947
+ :root {
27948
+ --navi-icon-align-y: center;
27949
+ }
27950
+
27951
+ .navi_text {
27952
+ display: inline-flex;
27953
+ align-items: baseline;
27954
+ gap: 0.1em;
27955
+ }
27956
+
27957
+ .navi_icon {
27958
+ --align-y: var(--navi-icon-align-y, center);
27959
+
27960
+ display: inline-flex;
27961
+ width: 1em;
27962
+ height: 1em;
27963
+ flex-shrink: 0;
27964
+ align-self: var(--align-y);
27965
+ line-height: 1em;
27966
+ }
27967
+ `;
27968
+ const Text = ({
27969
+ children,
27970
+ ...rest
27971
+ }) => {
27972
+ return jsx("span", {
27973
+ ...rest,
27974
+ className: "navi_text",
27975
+ children: children
27976
+ });
27977
+ };
27978
+ const alignYMapping = {
27979
+ start: "flex-start",
27980
+ center: "center",
27981
+ end: "flex-end"
27982
+ };
27983
+ const Icon = ({
27984
+ alignY,
27985
+ style,
27986
+ children,
27987
+ ...rest
27988
+ }) => {
27989
+ const innerStyle = {
27990
+ ...style
27991
+ };
27992
+ if (alignY !== "center") {
27993
+ innerStyle["--align-y"] = alignYMapping[alignY];
27994
+ }
27995
+ return jsx("span", {
27996
+ ...rest,
27997
+ className: "navi_icon",
27998
+ style: innerStyle,
27999
+ children: children
28000
+ });
28001
+ };
28002
+
27806
28003
  installImportMetaCss(import.meta);import.meta.css = /* css */`
27807
28004
  .text_and_count {
27808
28005
  display: flex;
@@ -27912,4 +28109,4 @@ const useDependenciesDiff = (inputs) => {
27912
28109
  return diffRef.current;
27913
28110
  };
27914
28111
 
27915
- export { ActionRenderer, ActiveKeyboardShortcuts, Button, Checkbox, CheckboxList, Col, Colgroup, Details, Editable, ErrorBoundaryContext, FontSizedSvg, Form, IconAndText, Input, Label, Link, LinkWithIcon, Overflow, Radio, RadioList, Route, RowNumberCol, RowNumberTableCell, SINGLE_SPACE_CONSTRAINT, SVGMaskOverlay, Select, SelectionContext, SummaryMarker, Tab, TabList, Table, TableCell, Tbody, TextAndCount, Thead, Tr, UITransition, actionIntegratedVia, addCustomMessage, createAction, createSelectionKeyboardShortcuts, createUniqueValueConstraint, defineRoutes, enableDebugActions, enableDebugOnDocumentLoading, goBack, goForward, goTo, isCellSelected, isColumnSelected, isRowSelected, openCallout, rawUrlPart, reload, removeCustomMessage, rerunActions, resource, setBaseUrl, stopLoad, stringifyTableSelectionValue, updateActions, useActionData, useActionStatus, useCellsAndColumns, useDependenciesDiff, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSignalSync, useStateArray, valueInLocalStorage };
28112
+ export { ActionRenderer, ActiveKeyboardShortcuts, Button, Checkbox, CheckboxList, Col, Colgroup, Details, Editable, ErrorBoundaryContext, FontSizedSvg, Form, Icon, IconAndText, Input, Label, Link, LinkWithIcon, Overflow, Radio, RadioList, Route, RowNumberCol, RowNumberTableCell, SINGLE_SPACE_CONSTRAINT, SVGMaskOverlay, Select, SelectionContext, SummaryMarker, Tab, TabList, Table, TableCell, Tbody, Text, TextAndCount, Thead, Tr, UITransition, actionIntegratedVia, addCustomMessage, createAction, createSelectionKeyboardShortcuts, createUniqueValueConstraint, defineRoutes, enableDebugActions, enableDebugOnDocumentLoading, goBack, goForward, goTo, isCellSelected, isColumnSelected, isRowSelected, openCallout, rawUrlPart, reload, removeCustomMessage, rerunActions, resource, setBaseUrl, stopLoad, stringifyTableSelectionValue, updateActions, useActionData, useActionStatus, useCellsAndColumns, useDependenciesDiff, useDocumentState, useDocumentUrl, useEditionController, useFocusGroup, useKeyboardShortcuts, useNavState, useRouteStatus, useRunOnMount, useSelectableElement, useSelectionController, useSignalSync, useStateArray, valueInLocalStorage };
package/index.js CHANGED
@@ -88,6 +88,7 @@ export { FontSizedSvg } from "./src/components/svg/font_sized_svg.jsx";
88
88
  export { IconAndText } from "./src/components/svg/icon_and_text.jsx";
89
89
  export { SVGMaskOverlay } from "./src/components/svg/svg_mask_overlay.jsx";
90
90
  export { Overflow } from "./src/components/text/overflow.jsx";
91
+ export { Icon, Text } from "./src/components/text/text.jsx";
91
92
  export { TextAndCount } from "./src/components/text/text_and_count.jsx";
92
93
  // Callout, dialogs, ...
93
94
  export { openCallout } from "./src/components/callout/callout.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.4.2",
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";
@@ -40,11 +42,15 @@ import.meta.css = /* css */ `
40
42
  @layer navi {
41
43
  .navi_button {
42
44
  position: relative;
43
- display: inline-block;
45
+ display: inline-flex;
46
+ width: fit-content;
47
+ height: fit-content;
44
48
  padding: 0;
45
49
  background: none;
46
50
  border: none;
51
+ border-radius: inherit;
47
52
  outline: none;
53
+ cursor: pointer;
48
54
 
49
55
  --border-width: 1px;
50
56
  --outline-width: 1px;
@@ -138,6 +144,9 @@ import.meta.css = /* css */ `
138
144
  --color: var(--color-readonly);
139
145
  }
140
146
  /* Disabled */
147
+ .navi_button[data-disabled] {
148
+ cursor: default;
149
+ }
141
150
  .navi_button[data-disabled] .navi_button_content {
142
151
  --border-color: var(--border-color-disabled);
143
152
  --background-color: var(--background-color-disabled);
@@ -188,6 +197,11 @@ export const Button = forwardRef((props, ref) => {
188
197
  });
189
198
  });
190
199
 
200
+ const alignXMapping = {
201
+ start: undefined,
202
+ center: "center",
203
+ end: "flex-end",
204
+ };
191
205
  const ButtonBasic = forwardRef((props, ref) => {
192
206
  const contextLoading = useContext(LoadingContext);
193
207
  const contextLoadingElement = useContext(LoadingElementContext);
@@ -199,8 +213,14 @@ const ButtonBasic = forwardRef((props, ref) => {
199
213
  loading,
200
214
  constraints = [],
201
215
  autoFocus,
216
+
217
+ // visual
202
218
  appearance = "navi",
219
+ alignX = "start",
203
220
  discrete,
221
+ className,
222
+ style,
223
+
204
224
  children,
205
225
  ...rest
206
226
  } = props;
@@ -221,11 +241,20 @@ const ButtonBasic = forwardRef((props, ref) => {
221
241
  buttonChildren = children;
222
242
  }
223
243
 
244
+ const innerStyle = {
245
+ "align-self": alignXMapping[alignX],
246
+ };
247
+
224
248
  return (
225
249
  <button
226
250
  {...rest}
227
251
  ref={innerRef}
228
- className={appearance === "navi" ? "navi_button" : undefined}
252
+ className={withPropsClassName(
253
+ appearance === "navi" ? "navi_button" : undefined,
254
+ className,
255
+ )}
256
+ style={withPropsStyle(innerStyle, style)}
257
+ disabled={innerDisabled}
229
258
  data-discrete={discrete ? "" : undefined}
230
259
  data-readonly={innerReadOnly ? "" : undefined}
231
260
  data-readonly-silent={innerLoading ? "" : 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
+ };
@@ -23,6 +23,7 @@
23
23
  export const FontSizedSvg = ({
24
24
  width = "1em",
25
25
  height = "1em",
26
+ style,
26
27
  children,
27
28
  ...props
28
29
  }) => {
@@ -30,6 +31,7 @@ export const FontSizedSvg = ({
30
31
  <span
31
32
  {...props}
32
33
  style={{
34
+ ...style,
33
35
  display: "flex",
34
36
  alignItems: "center",
35
37
  width,
@@ -0,0 +1,465 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Text and Icon Demo</title>
6
+ <script type="importmap">
7
+ {
8
+ "imports": {
9
+ "preact": "https://esm.sh/preact@10.22.0",
10
+ "preact/": "https://esm.sh/preact@10.22.0/"
11
+ }
12
+ }
13
+ </script>
14
+ </head>
15
+ <body>
16
+ <div id="app"></div>
17
+ <script type="module" jsenv-type="module/jsx">
18
+ import { render } from "preact";
19
+ /* eslint-disable no-unused-vars */
20
+ import { Text, Icon } from "../text.jsx";
21
+
22
+ // Sample SVG icons
23
+ const HomeIcon = () => (
24
+ <svg
25
+ viewBox="0 0 24 24"
26
+ width="100%"
27
+ height="100%"
28
+ xmlns="http://www.w3.org/2000/svg"
29
+ >
30
+ <path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" fill="currentColor" />
31
+ </svg>
32
+ );
33
+
34
+ const UserIcon = () => (
35
+ <svg
36
+ viewBox="0 0 24 24"
37
+ width="100%"
38
+ height="100%"
39
+ xmlns="http://www.w3.org/2000/svg"
40
+ >
41
+ <path
42
+ d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
43
+ fill="currentColor"
44
+ />
45
+ </svg>
46
+ );
47
+
48
+ const SettingsIcon = () => (
49
+ <svg
50
+ viewBox="0 0 24 24"
51
+ width="100%"
52
+ height="100%"
53
+ xmlns="http://www.w3.org/2000/svg"
54
+ >
55
+ <path
56
+ d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.82,11.69,4.82,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"
57
+ fill="currentColor"
58
+ />
59
+ </svg>
60
+ );
61
+
62
+ const SearchIcon = () => (
63
+ <svg
64
+ viewBox="0 0 24 24"
65
+ width="100%"
66
+ height="100%"
67
+ xmlns="http://www.w3.org/2000/svg"
68
+ >
69
+ <path
70
+ d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
71
+ fill="currentColor"
72
+ />
73
+ </svg>
74
+ );
75
+
76
+ const StarIcon = () => (
77
+ <svg
78
+ viewBox="0 0 24 24"
79
+ width="100%"
80
+ height="100%"
81
+ xmlns="http://www.w3.org/2000/svg"
82
+ >
83
+ <path
84
+ d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"
85
+ fill="currentColor"
86
+ />
87
+ </svg>
88
+ );
89
+
90
+ const HeartIcon = () => (
91
+ <svg
92
+ viewBox="0 0 24 24"
93
+ width="100%"
94
+ height="100%"
95
+ xmlns="http://www.w3.org/2000/svg"
96
+ >
97
+ <path
98
+ d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
99
+ fill="currentColor"
100
+ />
101
+ </svg>
102
+ );
103
+
104
+ const Demo = () => {
105
+ return (
106
+ <div
107
+ style={{
108
+ padding: "20px",
109
+ fontFamily: "system-ui, sans-serif",
110
+ lineHeight: "1.6",
111
+ maxWidth: "800px",
112
+ }}
113
+ >
114
+ <h1>Text and Icon Combinations</h1>
115
+
116
+ <section style={{ marginBottom: "40px" }}>
117
+ <h2>Basic Icon + Text</h2>
118
+ <div
119
+ style={{
120
+ display: "flex",
121
+ flexDirection: "column",
122
+ gap: "10px",
123
+ }}
124
+ >
125
+ <Text>
126
+ <Icon>
127
+ <HomeIcon />
128
+ </Icon>
129
+ Home
130
+ </Text>
131
+
132
+ <Text>
133
+ <Icon>
134
+ <UserIcon />
135
+ </Icon>
136
+ Profile
137
+ </Text>
138
+
139
+ <Text>
140
+ <Icon>
141
+ <SettingsIcon />
142
+ </Icon>
143
+ Settings
144
+ </Text>
145
+ </div>
146
+ </section>
147
+
148
+ <section style={{ marginBottom: "40px" }}>
149
+ <h2>Text + Icon (Reversed Order)</h2>
150
+ <div
151
+ style={{
152
+ display: "flex",
153
+ flexDirection: "column",
154
+ gap: "10px",
155
+ }}
156
+ >
157
+ <Text>
158
+ Search
159
+ <Icon>
160
+ <SearchIcon />
161
+ </Icon>
162
+ </Text>
163
+
164
+ <Text>
165
+ Favorites
166
+ <Icon>
167
+ <StarIcon />
168
+ </Icon>
169
+ </Text>
170
+
171
+ <Text>
172
+ Liked
173
+ <Icon>
174
+ <HeartIcon />
175
+ </Icon>
176
+ </Text>
177
+ </div>
178
+ </section>
179
+
180
+ <section style={{ marginBottom: "40px" }}>
181
+ <h2>Multiple Icons</h2>
182
+ <div
183
+ style={{
184
+ display: "flex",
185
+ flexDirection: "column",
186
+ gap: "10px",
187
+ }}
188
+ >
189
+ <Text>
190
+ <Icon>
191
+ <HomeIcon />
192
+ </Icon>
193
+ Left and right
194
+ <Icon>
195
+ <StarIcon />
196
+ </Icon>
197
+ </Text>
198
+
199
+ <Text>
200
+ <Icon>
201
+ <UserIcon />
202
+ </Icon>
203
+ <Icon>
204
+ <SettingsIcon />
205
+ </Icon>
206
+ Two on the left
207
+ </Text>
208
+ </div>
209
+ </section>
210
+
211
+ <section style={{ marginBottom: "40px" }}>
212
+ <h2>Different Font Sizes</h2>
213
+ <div
214
+ style={{
215
+ display: "flex",
216
+ flexDirection: "column",
217
+ gap: "15px",
218
+ }}
219
+ >
220
+ <Text style={{ fontSize: "12px" }}>
221
+ <Icon>
222
+ <HomeIcon />
223
+ </Icon>
224
+ Small text with icon
225
+ </Text>
226
+
227
+ <Text style={{ fontSize: "16px" }}>
228
+ <Icon>
229
+ <UserIcon />
230
+ </Icon>
231
+ Normal text with icon
232
+ </Text>
233
+
234
+ <Text style={{ fontSize: "24px" }}>
235
+ <Icon>
236
+ <SettingsIcon />
237
+ </Icon>
238
+ Large text with icon
239
+ </Text>
240
+
241
+ <Text style={{ fontSize: "32px" }}>
242
+ <Icon>
243
+ <StarIcon />
244
+ </Icon>
245
+ Extra large text
246
+ </Text>
247
+ </div>
248
+ </section>
249
+
250
+ <section style={{ marginBottom: "40px" }}>
251
+ <h2>Colored Examples</h2>
252
+ <div
253
+ style={{
254
+ display: "flex",
255
+ flexDirection: "column",
256
+ gap: "10px",
257
+ }}
258
+ >
259
+ <Text style={{ color: "blue" }}>
260
+ <Icon>
261
+ <HomeIcon />
262
+ </Icon>
263
+ Blue home link
264
+ </Text>
265
+
266
+ <Text style={{ color: "green" }}>
267
+ <Icon>
268
+ <UserIcon />
269
+ </Icon>
270
+ Green user profile
271
+ </Text>
272
+
273
+ <Text style={{ color: "red" }}>
274
+ <Icon>
275
+ <HeartIcon />
276
+ </Icon>
277
+ Red heart favorite
278
+ </Text>
279
+
280
+ <Text style={{ color: "purple" }}>
281
+ <Icon>
282
+ <StarIcon />
283
+ </Icon>
284
+ Purple star rating
285
+ </Text>
286
+ </div>
287
+ </section>
288
+
289
+ <section style={{ marginBottom: "40px" }}>
290
+ <h2>Interactive Buttons</h2>
291
+ <div
292
+ style={{
293
+ display: "flex",
294
+ flexDirection: "column",
295
+ gap: "10px",
296
+ }}
297
+ >
298
+ <button
299
+ style={{
300
+ padding: "10px 15px",
301
+ border: "1px solid #ccc",
302
+ borderRadius: "4px",
303
+ background: "white",
304
+ cursor: "pointer",
305
+ fontSize: "14px",
306
+ }}
307
+ >
308
+ <Text>
309
+ <Icon>
310
+ <SearchIcon />
311
+ </Icon>
312
+ Search Files
313
+ </Text>
314
+ </button>
315
+
316
+ <button
317
+ style={{
318
+ padding: "10px 15px",
319
+ border: "none",
320
+ borderRadius: "4px",
321
+ background: "#007bff",
322
+ color: "white",
323
+ cursor: "pointer",
324
+ fontSize: "14px",
325
+ }}
326
+ >
327
+ <Text>
328
+ <Icon>
329
+ <UserIcon />
330
+ </Icon>
331
+ Create Account
332
+ </Text>
333
+ </button>
334
+
335
+ <button
336
+ style={{
337
+ padding: "10px 15px",
338
+ border: "1px solid #dc3545",
339
+ borderRadius: "4px",
340
+ background: "transparent",
341
+ color: "#dc3545",
342
+ cursor: "pointer",
343
+ fontSize: "14px",
344
+ }}
345
+ >
346
+ <Text>
347
+ <Icon>
348
+ <HeartIcon />
349
+ </Icon>
350
+ Add to Favorites
351
+ </Text>
352
+ </button>
353
+ </div>
354
+ </section>
355
+
356
+ <section style={{ marginBottom: "40px" }}>
357
+ <h2>Navigation Menu Style</h2>
358
+ <nav
359
+ style={{
360
+ background: "#f8f9fa",
361
+ padding: "20px",
362
+ borderRadius: "8px",
363
+ }}
364
+ >
365
+ <div
366
+ style={{
367
+ display: "flex",
368
+ flexDirection: "column",
369
+ gap: "15px",
370
+ }}
371
+ >
372
+ <Text
373
+ style={{
374
+ cursor: "pointer",
375
+ padding: "8px 12px",
376
+ borderRadius: "4px",
377
+ transition: "background-color 0.2s",
378
+ }}
379
+ >
380
+ <Icon>
381
+ <HomeIcon />
382
+ </Icon>
383
+ Dashboard
384
+ </Text>
385
+
386
+ <Text
387
+ style={{
388
+ cursor: "pointer",
389
+ padding: "8px 12px",
390
+ borderRadius: "4px",
391
+ background: "#007bff",
392
+ color: "white",
393
+ }}
394
+ >
395
+ <Icon>
396
+ <UserIcon />
397
+ </Icon>
398
+ Users
399
+ </Text>
400
+
401
+ <Text
402
+ style={{
403
+ cursor: "pointer",
404
+ padding: "8px 12px",
405
+ borderRadius: "4px",
406
+ }}
407
+ >
408
+ <Icon>
409
+ <SettingsIcon />
410
+ </Icon>
411
+ Settings
412
+ </Text>
413
+ </div>
414
+ </nav>
415
+ </section>
416
+
417
+ <section>
418
+ <h2>Inline Text with Icons</h2>
419
+ <p style={{ fontSize: "16px", lineHeight: "1.6" }}>
420
+ Welcome to our platform! Click on the{" "}
421
+ <Text>
422
+ <Icon>
423
+ <HomeIcon />
424
+ </Icon>
425
+ home
426
+ </Text>{" "}
427
+ button to return to the dashboard, or visit your{" "}
428
+ <Text>
429
+ <Icon>
430
+ <UserIcon />
431
+ </Icon>
432
+ profile
433
+ </Text>{" "}
434
+ to update your settings. Don't forget to{" "}
435
+ <Text>
436
+ <Icon>
437
+ <StarIcon />
438
+ </Icon>
439
+ star
440
+ </Text>{" "}
441
+ your favorite items and{" "}
442
+ <Text>
443
+ <Icon>
444
+ <HeartIcon />
445
+ </Icon>
446
+ like
447
+ </Text>{" "}
448
+ the content you enjoy. Use the{" "}
449
+ <Text>
450
+ <Icon>
451
+ <SearchIcon />
452
+ </Icon>
453
+ search
454
+ </Text>{" "}
455
+ feature to find what you're looking for quickly.
456
+ </p>
457
+ </section>
458
+ </div>
459
+ );
460
+ };
461
+
462
+ render(<Demo />, document.getElementById("app"));
463
+ </script>
464
+ </body>
465
+ </html>
@@ -0,0 +1,48 @@
1
+ import.meta.css = /* css */ `
2
+ :root {
3
+ --navi-icon-align-y: center;
4
+ }
5
+
6
+ .navi_text {
7
+ display: inline-flex;
8
+ align-items: baseline;
9
+ gap: 0.1em;
10
+ }
11
+
12
+ .navi_icon {
13
+ --align-y: var(--navi-icon-align-y, center);
14
+
15
+ display: inline-flex;
16
+ width: 1em;
17
+ height: 1em;
18
+ flex-shrink: 0;
19
+ align-self: var(--align-y);
20
+ line-height: 1em;
21
+ }
22
+ `;
23
+
24
+ export const Text = ({ children, ...rest }) => {
25
+ return (
26
+ <span {...rest} className="navi_text">
27
+ {children}
28
+ </span>
29
+ );
30
+ };
31
+
32
+ const alignYMapping = {
33
+ start: "flex-start",
34
+ center: "center",
35
+ end: "flex-end",
36
+ };
37
+ export const Icon = ({ alignY, style, children, ...rest }) => {
38
+ const innerStyle = { ...style };
39
+ if (alignY !== "center") {
40
+ innerStyle["--align-y"] = alignYMapping[alignY];
41
+ }
42
+
43
+ return (
44
+ <span {...rest} className="navi_icon" style={innerStyle}>
45
+ {children}
46
+ </span>
47
+ );
48
+ };