@jsenv/navi 0.8.0 → 0.9.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.
@@ -1,97 +1,19 @@
1
1
  import { withPropsStyle } from "../props_composition/with_props_style.js";
2
-
3
- export const consumeSpacingProps = (props) => {
4
- const style = {};
5
-
6
- outer_spacing: {
7
- const margin = props.margin;
8
- const marginX = props.marginX;
9
- const marginY = props.marginY;
10
- const marginLeft = props.marginLeft;
11
- const marginRight = props.marginRight;
12
- const marginTop = props.marginTop;
13
- const marginBottom = props.marginBottom;
14
- delete props.margin;
15
- delete props.marginX;
16
- delete props.marginY;
17
- delete props.marginLeft;
18
- delete props.marginRight;
19
- delete props.marginTop;
20
- delete props.marginBottom;
21
-
22
- if (margin !== undefined) {
23
- style.margin = margin;
24
- }
25
- if (marginLeft !== undefined) {
26
- style.marginLeft = marginLeft;
27
- } else if (marginX !== undefined) {
28
- style.marginLeft = marginX;
29
- }
30
- if (marginRight !== undefined) {
31
- style.marginRight = marginRight;
32
- } else if (marginX !== undefined) {
33
- style.marginRight = marginX;
34
- }
35
- if (marginTop !== undefined) {
36
- style.marginTop = marginTop;
37
- } else if (marginY !== undefined) {
38
- style.marginTop = marginY;
39
- }
40
- if (marginBottom !== undefined) {
41
- style.marginBottom = marginBottom;
42
- } else if (marginY !== undefined) {
43
- style.marginBottom = marginY;
44
- }
45
- }
46
-
47
- inner_spacing: {
48
- const padding = props.padding;
49
- const paddingX = props.paddingX;
50
- const paddingY = props.paddingY;
51
- const paddingLeft = props.paddingLeft;
52
- const paddingRight = props.paddingRight;
53
- const paddingTop = props.paddingTop;
54
- const paddingBottom = props.paddingBottom;
55
- delete props.padding;
56
- delete props.paddingX;
57
- delete props.paddingY;
58
- delete props.paddingLeft;
59
- delete props.paddingRight;
60
- delete props.paddingTop;
61
- delete props.paddingBottom;
62
-
63
- if (padding !== undefined) {
64
- style.padding = padding;
65
- }
66
- if (paddingLeft !== undefined) {
67
- style.paddingLeft = paddingLeft;
68
- } else if (paddingX !== undefined) {
69
- style.paddingLeft = paddingX;
70
- }
71
- if (paddingRight !== undefined) {
72
- style.paddingRight = paddingRight;
73
- } else if (paddingX !== undefined) {
74
- style.paddingRight = paddingX;
75
- }
76
- if (paddingTop !== undefined) {
77
- style.paddingTop = paddingTop;
78
- } else if (paddingY !== undefined) {
79
- style.paddingTop = paddingY;
80
- }
81
- if (paddingBottom !== undefined) {
82
- style.paddingBottom = paddingBottom;
83
- } else if (paddingY !== undefined) {
84
- style.paddingBottom = paddingY;
85
- }
86
- }
87
-
88
- return style;
89
- };
2
+ import { useLayoutStyle } from "./use_layout_style.js";
90
3
 
91
4
  export const Spacing = ({ style, children, ...rest }) => {
92
- const styleForSpacing = consumeSpacingProps(rest);
5
+ const { padding, margin } = useLayoutStyle(rest);
93
6
  return (
94
- <div {...rest} style={withPropsStyle(styleForSpacing, style)}>
7
+ <div
8
+ {...rest}
9
+ style={withPropsStyle(
10
+ {
11
+ ...margin,
12
+ ...padding,
13
+ },
14
+ style,
15
+ )}
16
+ >
95
17
  {children}
96
18
  </div>
97
19
  );
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Layout Style Hook
3
+ *
4
+ * This hook processes layout-related props and converts them into CSS styles.
5
+ * It handles spacing (margin/padding), alignment (alignX/alignY), and expansion behavior.
6
+ * The hook is context-aware and adapts behavior based on flex direction.
7
+ *
8
+ * Key features:
9
+ * - Spacing: margin/padding with X/Y shortcuts and directional variants
10
+ * - Alignment: alignX/alignY using align-self and auto margins
11
+ * - Expansion: expand prop for taking remaining space (flexGrow or width: 100%)
12
+ * - Context-aware: behavior changes based on FlexDirectionContext (row/column/none)
13
+ */
14
+
15
+ import { useContext } from "preact/hooks";
16
+
17
+ import { FlexDirectionContext } from "./layout_context.jsx";
18
+
19
+ /**
20
+ * Converts layout props into CSS styles
21
+ * @param {Object} props - Component props containing layout properties
22
+ * @param {string|number} [props.margin] - All-sides margin
23
+ * @param {string|number} [props.marginX] - Horizontal margin (left + right)
24
+ * @param {string|number} [props.marginY] - Vertical margin (top + bottom)
25
+ * @param {string|number} [props.marginLeft] - Left margin
26
+ * @param {string|number} [props.marginRight] - Right margin
27
+ * @param {string|number} [props.marginTop] - Top margin
28
+ * @param {string|number} [props.marginBottom] - Bottom margin
29
+ * @param {string|number} [props.padding] - All-sides padding
30
+ * @param {string|number} [props.paddingX] - Horizontal padding (left + right)
31
+ * @param {string|number} [props.paddingY] - Vertical padding (top + bottom)
32
+ * @param {string|number} [props.paddingLeft] - Left padding
33
+ * @param {string|number} [props.paddingRight] - Right padding
34
+ * @param {string|number} [props.paddingTop] - Top padding
35
+ * @param {string|number} [props.paddingBottom] - Bottom padding
36
+ * @param {"start"|"center"|"end"|"stretch"} [props.alignX] - Horizontal alignment
37
+ * @param {"start"|"center"|"end"|"stretch"} [props.alignY] - Vertical alignment
38
+ * @param {boolean} [props.expandX] - Whether element should expand horizontally to fill available space
39
+ * @param {boolean} [props.expandY] - Whether element should expand vertically to fill available space
40
+ * @returns {Object} Object with categorized styles: { margin, padding, alignment, expansion, all }
41
+ */
42
+ export const useLayoutStyle = (props) => {
43
+ const flexDirection = useContext(FlexDirectionContext);
44
+
45
+ const marginStyle = {};
46
+ const paddingStyle = {};
47
+ const alignmentStyle = {};
48
+ const expansionStyle = {};
49
+
50
+ spacing: {
51
+ outer_spacing: {
52
+ const margin = props.margin;
53
+ const marginX = props.marginX;
54
+ const marginY = props.marginY;
55
+ const marginLeft = props.marginLeft;
56
+ const marginRight = props.marginRight;
57
+ const marginTop = props.marginTop;
58
+ const marginBottom = props.marginBottom;
59
+ delete props.margin;
60
+ delete props.marginX;
61
+ delete props.marginY;
62
+ delete props.marginLeft;
63
+ delete props.marginRight;
64
+ delete props.marginTop;
65
+ delete props.marginBottom;
66
+
67
+ if (margin !== undefined) {
68
+ marginStyle.margin = margin;
69
+ }
70
+ if (marginLeft !== undefined) {
71
+ marginStyle.marginLeft = marginLeft;
72
+ } else if (marginX !== undefined) {
73
+ marginStyle.marginLeft = marginX;
74
+ }
75
+ if (marginRight !== undefined) {
76
+ marginStyle.marginRight = marginRight;
77
+ } else if (marginX !== undefined) {
78
+ marginStyle.marginRight = marginX;
79
+ }
80
+ if (marginTop !== undefined) {
81
+ marginStyle.marginTop = marginTop;
82
+ } else if (marginY !== undefined) {
83
+ marginStyle.marginTop = marginY;
84
+ }
85
+ if (marginBottom !== undefined) {
86
+ marginStyle.marginBottom = marginBottom;
87
+ } else if (marginY !== undefined) {
88
+ marginStyle.marginBottom = marginY;
89
+ }
90
+ }
91
+ inner_spacing: {
92
+ const padding = props.padding;
93
+ const paddingX = props.paddingX;
94
+ const paddingY = props.paddingY;
95
+ const paddingLeft = props.paddingLeft;
96
+ const paddingRight = props.paddingRight;
97
+ const paddingTop = props.paddingTop;
98
+ const paddingBottom = props.paddingBottom;
99
+ delete props.padding;
100
+ delete props.paddingX;
101
+ delete props.paddingY;
102
+ delete props.paddingLeft;
103
+ delete props.paddingRight;
104
+ delete props.paddingTop;
105
+ delete props.paddingBottom;
106
+
107
+ if (padding !== undefined) {
108
+ paddingStyle.padding = padding;
109
+ }
110
+ if (paddingLeft !== undefined) {
111
+ paddingStyle.paddingLeft = paddingLeft;
112
+ } else if (paddingX !== undefined) {
113
+ paddingStyle.paddingLeft = paddingX;
114
+ }
115
+ if (paddingRight !== undefined) {
116
+ paddingStyle.paddingRight = paddingRight;
117
+ } else if (paddingX !== undefined) {
118
+ paddingStyle.paddingRight = paddingX;
119
+ }
120
+ if (paddingTop !== undefined) {
121
+ paddingStyle.paddingTop = paddingTop;
122
+ } else if (paddingY !== undefined) {
123
+ paddingStyle.paddingTop = paddingY;
124
+ }
125
+ if (paddingBottom !== undefined) {
126
+ paddingStyle.paddingBottom = paddingBottom;
127
+ } else if (paddingY !== undefined) {
128
+ paddingStyle.paddingBottom = paddingY;
129
+ }
130
+ }
131
+ }
132
+
133
+ align: {
134
+ const alignX = props.alignX;
135
+ const alignY = props.alignY;
136
+ delete props.alignX;
137
+ delete props.alignY;
138
+
139
+ // flex
140
+ if (flexDirection === "row") {
141
+ // In row direction: alignX controls justify-content, alignY controls align-self
142
+ if (alignY !== undefined && alignY !== "start") {
143
+ alignmentStyle.alignSelf = alignY;
144
+ }
145
+ // For row, alignX uses auto margins for positioning
146
+ // NOTE: Auto margins only work effectively for positioning individual items.
147
+ // When multiple adjacent items have the same auto margin alignment (e.g., alignX="end"),
148
+ // only the first item will be positioned as expected because subsequent items
149
+ // will be positioned relative to the previous item's margins, not the container edge.
150
+ if (alignX !== undefined) {
151
+ if (alignX === "start") {
152
+ alignmentStyle.marginRight = "auto";
153
+ } else if (alignX === "end") {
154
+ alignmentStyle.marginLeft = "auto";
155
+ } else if (alignX === "center") {
156
+ alignmentStyle.marginLeft = "auto";
157
+ alignmentStyle.marginRight = "auto";
158
+ }
159
+ }
160
+ } else if (flexDirection === "column") {
161
+ // In column direction: alignX controls align-self, alignY uses auto margins
162
+ if (alignX !== undefined && alignX !== "start") {
163
+ alignmentStyle.alignSelf = alignX;
164
+ }
165
+ // For column, alignY uses auto margins for positioning
166
+ // NOTE: Same auto margin limitation applies - multiple adjacent items with
167
+ // the same alignY won't all position relative to container edges.
168
+ if (alignY !== undefined) {
169
+ if (alignY === "start") {
170
+ alignmentStyle.marginBottom = "auto";
171
+ } else if (alignY === "end") {
172
+ alignmentStyle.marginTop = "auto";
173
+ } else if (alignY === "center") {
174
+ alignmentStyle.marginTop = "auto";
175
+ alignmentStyle.marginBottom = "auto";
176
+ }
177
+ }
178
+ }
179
+ // non flex
180
+ else {
181
+ if (alignX === "start") {
182
+ alignmentStyle.marginRight = "auto";
183
+ } else if (alignX === "center") {
184
+ alignmentStyle.marginLeft = "auto";
185
+ alignmentStyle.marginRight = "auto";
186
+ } else if (alignX === "end") {
187
+ alignmentStyle.marginLeft = "auto";
188
+ }
189
+
190
+ if (alignY === "start") {
191
+ alignmentStyle.marginBottom = "auto";
192
+ } else if (alignY === "center") {
193
+ alignmentStyle.marginTop = "auto";
194
+ alignmentStyle.marginBottom = "auto";
195
+ } else if (alignY === "end") {
196
+ alignmentStyle.marginTop = "auto";
197
+ }
198
+ }
199
+ }
200
+
201
+ expand: {
202
+ const expand = props.expand;
203
+ delete props.expand;
204
+
205
+ expandX: {
206
+ const expandX = props.expandX || expand;
207
+ delete props.expandX;
208
+ if (expandX) {
209
+ if (flexDirection === "row") {
210
+ expansionStyle.flexGrow = 1; // Grow horizontally in row
211
+ } else if (flexDirection === "column") {
212
+ expansionStyle.width = "100%"; // Take full width in column
213
+ } else {
214
+ expansionStyle.width = "100%"; // Take full width outside flex
215
+ }
216
+ }
217
+ }
218
+
219
+ expandY: {
220
+ const expandY = props.expandY || expand;
221
+ delete props.expandY;
222
+ if (expandY) {
223
+ if (flexDirection === "row") {
224
+ expansionStyle.height = "100%"; // Take full height in row
225
+ } else if (flexDirection === "column") {
226
+ expansionStyle.flexGrow = 1; // Grow vertically in column
227
+ } else {
228
+ expansionStyle.height = "100%"; // Take full height outside flex
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ // Merge all styles for convenience
235
+ const allStyles = {
236
+ ...marginStyle,
237
+ ...paddingStyle,
238
+ ...alignmentStyle,
239
+ ...expansionStyle,
240
+ };
241
+
242
+ return {
243
+ margin: marginStyle,
244
+ padding: paddingStyle,
245
+ alignment: alignmentStyle,
246
+ expansion: expansionStyle,
247
+ all: allStyles,
248
+ };
249
+ };
@@ -12,7 +12,10 @@ import { useConstraints } from "../../validation/hooks/use_constraints.js";
12
12
  import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
13
13
  import { useRequestedActionStatus } from "../field/use_action_events.js";
14
14
  import { useKeyboardShortcuts } from "../keyboard_shortcuts/keyboard_shortcuts.js";
15
+ import { useLayoutStyle } from "../layout/use_layout_style.js";
15
16
  import { LoadableInlineElement } from "../loader/loader_background.jsx";
17
+ import { withPropsClassName } from "../props_composition/with_props_class_name.js";
18
+ import { withPropsStyle } from "../props_composition/with_props_style.js";
16
19
  import {
17
20
  SelectionContext,
18
21
  useSelectableElement,
@@ -90,7 +93,6 @@ const LinkBasic = forwardRef((props, ref) => {
90
93
  });
91
94
  const LinkPlain = forwardRef((props, ref) => {
92
95
  const {
93
- className = "",
94
96
  loading,
95
97
  readOnly,
96
98
  disabled,
@@ -103,6 +105,10 @@ const LinkPlain = forwardRef((props, ref) => {
103
105
  onClick,
104
106
  onKeyDown,
105
107
  href,
108
+
109
+ // visual
110
+ className,
111
+ style,
106
112
  ...rest
107
113
  } = props;
108
114
  const innerRef = useRef();
@@ -114,6 +120,10 @@ const LinkPlain = forwardRef((props, ref) => {
114
120
  const shouldDimColor = readOnly || disabled;
115
121
  useDimColorWhen(innerRef, shouldDimColor);
116
122
 
123
+ const innerClassName = withPropsClassName("navi_link", className);
124
+ const { all } = useLayoutStyle(rest);
125
+ const innerStyle = withPropsStyle(all, style);
126
+
117
127
  return (
118
128
  <LoadableInlineElement
119
129
  loading={loading}
@@ -123,7 +133,8 @@ const LinkPlain = forwardRef((props, ref) => {
123
133
  {...rest}
124
134
  ref={innerRef}
125
135
  href={href}
126
- className={["navi_link", ...className.split(" ")].join(" ")}
136
+ className={innerClassName}
137
+ style={innerStyle}
127
138
  aria-busy={loading}
128
139
  inert={disabled}
129
140
  data-field=""
@@ -1,4 +1,4 @@
1
- import { consumeSpacingProps } from "../layout/spacing.jsx";
1
+ import { useLayoutStyle } from "../layout/use_layout_style.js";
2
2
  import { withPropsStyle } from "../props_composition/with_props_style.js";
3
3
 
4
4
  import.meta.css = /* css */ `
@@ -31,28 +31,16 @@ export const Text = ({
31
31
  italic,
32
32
  underline,
33
33
  style,
34
- alignX,
35
34
  ...rest
36
35
  }) => {
36
+ const { all } = useLayoutStyle(rest);
37
37
  const innerStyle = withPropsStyle(
38
38
  {
39
+ ...all,
39
40
  color,
40
41
  fontWeight: bold ? "bold" : undefined,
41
42
  fontStyle: italic ? "italic" : undefined,
42
43
  textDecoration: underline ? "underline" : undefined,
43
- ...consumeSpacingProps(rest),
44
- ...(alignX === "start"
45
- ? {}
46
- : alignX === "center"
47
- ? {
48
- alignSelf: "center",
49
- marginLeft: "auto",
50
- marginRight: "auto",
51
- }
52
- : {
53
- alignSelf: "end",
54
- marginLeft: "auto",
55
- }),
56
44
  },
57
45
  style,
58
46
  );