flet-box 1.0.6 → 1.0.8

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.
Files changed (162) hide show
  1. package/README.md +28 -1
  2. package/bin/cli.js +90 -11
  3. package/bin/commands/auditFix.js +682 -0
  4. package/bin/commands/checkApi.js +20 -6
  5. package/bin/commands/checkUi.js +1369 -0
  6. package/bin/commands/create.js +86 -12
  7. package/bin/commands/createBundle.js +75 -1
  8. package/bin/commands/images.js +349 -0
  9. package/bin/commands/pruneAssets.js +170 -0
  10. package/bin/commands/release.js +34 -1
  11. package/bin/commands/runCloudflare.js +47 -9
  12. package/bin/commands/runServer.js +91 -25
  13. package/bin/commands/toPng.js +346 -0
  14. package/bin/commands/unpack.js +1173 -0
  15. package/bin/ota/loader.js +90 -0
  16. package/bin/utils/appSources.js +101 -0
  17. package/bin/utils/bundleConfig.js +166 -0
  18. package/bin/utils/dtsProps.js +147 -0
  19. package/bin/utils/imageTools.js +271 -0
  20. package/bin/utils/images/bmp.js +99 -0
  21. package/bin/utils/images/gif.js +244 -0
  22. package/bin/utils/images/index.js +163 -0
  23. package/bin/utils/images/jpeg.js +643 -0
  24. package/bin/utils/images/png.js +486 -0
  25. package/bin/utils/images/unsupported.js +29 -0
  26. package/bin/utils/neutralise.js +109 -6
  27. package/bin/utils/pwaShell.js +137 -0
  28. package/bin/utils/templates.js +1653 -471
  29. package/docs/README.md +2 -0
  30. package/docs/cli/README.md +144 -1
  31. package/docs/guides/README.md +2 -0
  32. package/docs/guides/clean-ui.md +211 -0
  33. package/docs/guides/minification-and-protection.md +1 -1
  34. package/docs/guides/online-updates.md +105 -9
  35. package/docs/tools/README.md +1 -1
  36. package/docs/tools/animation.md +1 -1
  37. package/docs/tools/markdown.md +3 -3
  38. package/docs/tools/theme.md +41 -1
  39. package/docs/widget/Accordion.md +4 -0
  40. package/docs/widget/AdMob.md +5 -1
  41. package/docs/widget/AdSense.md +4 -0
  42. package/docs/widget/AnimatedBox.md +7 -3
  43. package/docs/widget/AnimatedText.md +4 -0
  44. package/docs/widget/Audio.md +5 -1
  45. package/docs/widget/Avatar.md +15 -3
  46. package/docs/widget/Badge.md +4 -0
  47. package/docs/widget/Button.md +4 -0
  48. package/docs/widget/COMMON_PROPS.md +52 -0
  49. package/docs/widget/Card.md +4 -0
  50. package/docs/widget/Carousel.md +4 -0
  51. package/docs/widget/Chart.md +4 -0
  52. package/docs/widget/Checkbox.md +4 -0
  53. package/docs/widget/Chip.md +4 -0
  54. package/docs/widget/CircularBar.md +4 -0
  55. package/docs/widget/CircularChart.md +4 -0
  56. package/docs/widget/CodeViewer.md +4 -0
  57. package/docs/widget/Column.md +4 -0
  58. package/docs/widget/Container.md +4 -0
  59. package/docs/widget/DataTable.md +4 -0
  60. package/docs/widget/Divider.md +4 -0
  61. package/docs/widget/DraggBox.md +6 -2
  62. package/docs/widget/Dropdown.md +6 -0
  63. package/docs/widget/DroppBox.md +5 -1
  64. package/docs/widget/FloatingActionButton.md +4 -0
  65. package/docs/widget/GridView.md +4 -0
  66. package/docs/widget/Icon.md +4 -0
  67. package/docs/widget/Image.md +14 -3
  68. package/docs/widget/Input.md +4 -0
  69. package/docs/widget/Inspector.md +2 -2
  70. package/docs/widget/InstallButton.md +4 -0
  71. package/docs/widget/ListTile.md +4 -0
  72. package/docs/widget/ListView.md +4 -0
  73. package/docs/widget/Maps.md +191 -0
  74. package/docs/widget/Markdown.md +4 -0
  75. package/docs/widget/MatrixRain.md +4 -0
  76. package/docs/widget/Pagination.md +4 -0
  77. package/docs/widget/ParallaxBox.md +4 -0
  78. package/docs/widget/ProgressBar.md +4 -0
  79. package/docs/widget/QRCode.md +4 -0
  80. package/docs/widget/README.md +12 -10
  81. package/docs/widget/Radio.md +5 -1
  82. package/docs/widget/Rating.md +4 -0
  83. package/docs/widget/Row.md +4 -0
  84. package/docs/widget/Skeleton.md +4 -0
  85. package/docs/widget/Slider.md +4 -0
  86. package/docs/widget/Stack.md +4 -0
  87. package/docs/widget/Stepper.md +4 -0
  88. package/docs/widget/Switch.md +4 -0
  89. package/docs/widget/Tabs.md +5 -5
  90. package/docs/widget/Text.md +4 -0
  91. package/docs/widget/Tooltip.md +14 -13
  92. package/docs/widget/TreeView.md +4 -0
  93. package/docs/widget/Video.md +6 -2
  94. package/package.json +2 -2
  95. package/src/animations/AnimatedBox.js +4 -1
  96. package/src/animations/MatrixRain.js +3 -3
  97. package/src/core/hmr-client.js +129 -34
  98. package/src/index.d.ts +195 -7
  99. package/src/index.js +2 -0
  100. package/src/navigations/AppBar.js +22 -16
  101. package/src/navigations/BottomNavigation.js +8 -13
  102. package/src/navigations/CollapsibleSideBar.js +30 -8
  103. package/src/navigations/Drawer.js +25 -15
  104. package/src/navigations/DrawerItem.js +3 -1
  105. package/src/navigations/Scaffold.js +26 -12
  106. package/src/navigations/Tabs.js +104 -59
  107. package/src/tools/color.js +54 -0
  108. package/src/tools/index.js +9 -1
  109. package/src/tools/scale.js +41 -0
  110. package/src/tools/shadow.js +20 -0
  111. package/src/utils/index.js +1 -0
  112. package/src/utils/readableInk.js +19 -0
  113. package/src/utils/syntaxHighlight.js +78 -10
  114. package/src/utils/themes.js +217 -54
  115. package/src/utils/units.js +33 -11
  116. package/src/widget-factory/effects.js +56 -5
  117. package/src/widget-factory/processProps.js +10 -1
  118. package/src/widget-factory/remTool.js +15 -29
  119. package/src/widget-factory/stackPosition.js +18 -3
  120. package/src/widget-factory/translateProps.js +60 -1
  121. package/src/widgets/Accordion.js +20 -23
  122. package/src/widgets/AdMob.js +7 -2
  123. package/src/widgets/Avatar.js +236 -133
  124. package/src/widgets/Badge.js +3 -2
  125. package/src/widgets/BottomSheet.js +14 -13
  126. package/src/widgets/Button.js +69 -30
  127. package/src/widgets/Card.js +6 -2
  128. package/src/widgets/Carousel.js +12 -10
  129. package/src/widgets/Chart.js +28 -5
  130. package/src/widgets/Checkbox.js +76 -18
  131. package/src/widgets/Chip.js +31 -10
  132. package/src/widgets/CircularBar.js +7 -7
  133. package/src/widgets/CircularChart.js +26 -8
  134. package/src/widgets/CodeViewer.js +93 -20
  135. package/src/widgets/Container.js +13 -0
  136. package/src/widgets/DataTable.js +17 -11
  137. package/src/widgets/DraggBox.js +2 -1
  138. package/src/widgets/Dropdown.js +59 -14
  139. package/src/widgets/DroppBox.js +4 -2
  140. package/src/widgets/FloatingActionButton.js +13 -7
  141. package/src/widgets/Icon.js +3 -1
  142. package/src/widgets/Image.js +102 -3
  143. package/src/widgets/Input.js +30 -23
  144. package/src/widgets/Inspector.js +54 -1
  145. package/src/widgets/InstallButton.js +6 -4
  146. package/src/widgets/ListTile.js +4 -3
  147. package/src/widgets/ListView.js +30 -12
  148. package/src/widgets/Maps.js +1495 -0
  149. package/src/widgets/Markdown.js +37 -26
  150. package/src/widgets/Modal.js +14 -11
  151. package/src/widgets/Pagination.js +16 -11
  152. package/src/widgets/ProgressBar.js +6 -4
  153. package/src/widgets/Radio.js +73 -15
  154. package/src/widgets/Rating.js +24 -6
  155. package/src/widgets/Skeleton.js +31 -31
  156. package/src/widgets/Slider.js +29 -18
  157. package/src/widgets/SnackBar.js +35 -39
  158. package/src/widgets/Stepper.js +13 -9
  159. package/src/widgets/Switch.js +59 -18
  160. package/src/widgets/Tooltip.js +24 -12
  161. package/src/widgets/TreeView.js +27 -15
  162. package/src/widgets/index.js +1 -0
@@ -4,6 +4,9 @@
4
4
  */
5
5
  import { WidgetFactory } from "../widget-factory/index.js";
6
6
  import { colors } from "../utils/themes.js";
7
+ import { readableInk } from "../utils/readableInk.js";
8
+
9
+ import { toREM } from "../utils/units.js";
7
10
 
8
11
  export const CircularChart = (props) => {
9
12
  let {
@@ -21,7 +24,10 @@ export const CircularChart = (props) => {
21
24
  centerContent = null,
22
25
  showLabels = false,
23
26
  labelSize = 12,
24
- labelColor = colors.text,
27
+ // Unset by default: every slice has its own fill, and the theme's ink is
28
+ // the same colour as the primary slice in both palettes, so the label has
29
+ // to be chosen against the slice it sits on (see `labelInk`).
30
+ labelColor = null,
25
31
  onClick = null,
26
32
  onHover = null,
27
33
  defaultColors = [
@@ -42,7 +48,7 @@ export const CircularChart = (props) => {
42
48
  borderColor = null,
43
49
  borderWidth = 0,
44
50
  sliceBorderWidth = 0,
45
- sliceBorderColor = "#ffffff",
51
+ sliceBorderColor = colors.card,
46
52
  ...rest
47
53
  } = props;
48
54
 
@@ -117,10 +123,15 @@ export const CircularChart = (props) => {
117
123
  const updateCanvasSize = () => {
118
124
  if (!canvas) return;
119
125
  if (isFixedSize) {
120
- canvas.width = canvasWidth;
121
- canvas.height = canvasHeight;
122
- currentWidth = canvasWidth;
123
- currentHeight = canvasHeight;
126
+ // The box is routed through rem, so `size: 160` is 240px wide for a reader
127
+ // on a 24px base font. The buffer has to follow the rendered box or the
128
+ // chart grows and turns soft; the numbers are the fallback for a node
129
+ // that has no layout yet.
130
+ const { width, height } = container.getBoundingClientRect();
131
+ canvas.width = width > 0 ? width : canvasWidth;
132
+ canvas.height = height > 0 ? height : canvasHeight;
133
+ currentWidth = canvas.width;
134
+ currentHeight = canvas.height;
124
135
  } else {
125
136
  const rect = container.getBoundingClientRect();
126
137
  let w = rect.width;
@@ -192,6 +203,13 @@ export const CircularChart = (props) => {
192
203
  return defaultColors[index % defaultColors.length];
193
204
  };
194
205
 
206
+ const labelInk = (fill) => {
207
+ if (labelColor) return labelColor;
208
+ if (fill === colors.primary) return colors.onPrimary;
209
+ if (fill === colors.secondary) return colors.onSecondary;
210
+ return readableInk(fill, colors.text);
211
+ };
212
+
195
213
  // Draw each segment with rounded ends (each one with its own color)
196
214
  const drawRoundedSegment = (slice, index) => {
197
215
  const r = getRadiusVal() / 2;
@@ -269,7 +287,7 @@ export const CircularChart = (props) => {
269
287
  const x = cx + labelRadius * Math.cos(midAngle);
270
288
  const y = cy + labelRadius * Math.sin(midAngle);
271
289
  ctx.font = `${labelSize}px system-ui`;
272
- ctx.fillStyle = labelColor;
290
+ ctx.fillStyle = labelInk(color);
273
291
  ctx.shadowBlur = 0;
274
292
  const percent = Math.round(
275
293
  (slice.value / (slice.end - slice.start)) * 100,
@@ -463,7 +481,7 @@ export const CircularChart = (props) => {
463
481
  position: "relative",
464
482
  overflow: "hidden",
465
483
  borderRadius:
466
- typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius,
484
+ toREM(borderRadius),
467
485
  border:
468
486
  borderWidth > 0 && borderColor
469
487
  ? `${borderWidth}px solid ${borderColor}`
@@ -5,7 +5,10 @@
5
5
  import { WidgetFactory } from "../widget-factory/index.js";
6
6
  import { colors } from "../utils/themes.js";
7
7
  import { composeUpdate } from "../utils/composeUpdate.js";
8
- import { generateHighlightedHtml } from "../utils/syntaxHighlight.js";
8
+ import { generateHighlightedHtml, syntaxPaletteFor } from "../utils/syntaxHighlight.js";
9
+ import { clipboard } from "../tools/clipboard.js";
10
+
11
+ import { toREM } from "../utils/units.js";
9
12
 
10
13
  export const CodeViewer = (props) => {
11
14
  const {
@@ -20,22 +23,32 @@ code = "",
20
23
  showLineNumbers = false,
21
24
  startingLineNumber = 1,
22
25
  lineNumberWidth = 40,
23
- lineNumberColor = colors.secondary,
26
+ lineNumberColor,
24
27
  ...rest
25
28
  } = props;
26
29
 
30
+ // The token map follows the block's own surface: the light theme's #F4F4F5
31
+ // pane needs the light map, and a caller that paints a dark pane on the light
32
+ // theme still gets spans that read.
33
+ const syntaxPalette = syntaxPaletteFor(backgroundColor);
34
+ // The gutter is not a token: it takes the muted ink of the pane's palette, so
35
+ // a custom pane does not get the page theme's grey on top of it.
36
+ const gutterInk = lineNumberColor || syntaxPalette.muted;
37
+
27
38
  let currentCode =
28
39
  typeof code === "string" ? code : JSON.stringify(code, null, 2);
29
40
  let currentTitle = title;
30
41
  let headerRef = null;
42
+ let headerTitleRef = null;
31
43
  let scrollWrapperRef = null;
32
44
  let lineNumbersColRef = null;
33
45
  let codeColRef = null;
34
46
  let codeWrapperRef = null;
47
+ let copyResetTimer = null;
35
48
 
36
49
  const refreshContent = () => {
37
50
  const lines = currentCode.split("\n");
38
- const highlightedLines = lines.map((line) => generateHighlightedHtml(line));
51
+ const highlightedLines = lines.map((line) => generateHighlightedHtml(line, syntaxPalette));
39
52
 
40
53
  if (showLineNumbers) {
41
54
  // Update line numbers
@@ -44,7 +57,7 @@ code = "",
44
57
  for (let i = 0; i < lines.length; i++) {
45
58
  const numberDiv = WidgetFactory({
46
59
  tag: "div",
47
- padding: `0 ${padding / 2}px 0 ${padding}px`,
60
+ padding: `0 ${toREM(padding / 2)} 0 ${toREM(padding)}`,
48
61
  whiteSpace: "pre",
49
62
  textContent: String(startingLineNumber + i),
50
63
  });
@@ -79,28 +92,86 @@ code = "",
79
92
  width: "100%",
80
93
  display: "flex",
81
94
  flexDirection: "column",
95
+ position: "relative",
82
96
  ...rest,
83
97
  });
84
98
 
99
+ // The copy button carries its own solid plate, so it reads on the primary
100
+ // header and on any caller-painted pane, and it never relies on the page ink.
101
+ const copyButtonRef = WidgetFactory({
102
+ tag: "button",
103
+ type: "button",
104
+ textContent: "Copy",
105
+ backgroundColor: colors.card,
106
+ color: colors.text,
107
+ border: `1px solid ${colors.border}`,
108
+ borderRadius: 6,
109
+ padding: "2px 8px",
110
+ fontFamily: "monospace",
111
+ fontSize: 11,
112
+ lineHeight: "1.6",
113
+ flexShrink: 0,
114
+ cursor: "pointer",
115
+ onclick: () => {
116
+ clipboard.copy(currentCode);
117
+ copyButtonRef.textContent = "Copied";
118
+ clearTimeout(copyResetTimer);
119
+ copyResetTimer = setTimeout(() => {
120
+ copyButtonRef.textContent = "Copy";
121
+ }, 1500);
122
+ },
123
+ });
124
+
125
+ // Inside the header when there is one; floating over the block otherwise.
126
+ const attachCopyButton = () => {
127
+ if (headerRef) {
128
+ copyButtonRef.style.position = "";
129
+ copyButtonRef.style.top = "";
130
+ copyButtonRef.style.right = "";
131
+ copyButtonRef.style.zIndex = "";
132
+ headerRef.appendChild(copyButtonRef);
133
+ } else {
134
+ copyButtonRef.style.position = "absolute";
135
+ copyButtonRef.style.top = toREM(8);
136
+ copyButtonRef.style.right = toREM(8);
137
+ copyButtonRef.style.zIndex = "3";
138
+ container.appendChild(copyButtonRef);
139
+ }
140
+ };
141
+
85
142
  // Header. Built through a helper so a later `title` update can create one on a
86
143
  // widget that was constructed without it.
87
144
  const ensureHeader = (text) => {
88
145
  if (!showHeader || !text) return null;
89
146
  if (headerRef) {
90
- headerRef.textContent = text;
147
+ headerTitleRef.textContent = text;
91
148
  return headerRef;
92
149
  }
93
150
  headerRef = WidgetFactory({
94
151
  tag: "div",
152
+ display: "flex",
153
+ alignItems: "center",
154
+ justifyContent: "space-between",
155
+ gap: 8,
95
156
  padding: "8px 12px",
96
157
  backgroundColor: colors.primary,
97
- borderRadius: `${borderRadius}px ${borderRadius}px 0 0`,
98
- color: "#fff",
158
+ borderRadius: `${toREM(borderRadius)} ${toREM(borderRadius)} 0 0`,
159
+ // The header is a primary plate; in dark mode that plate is near-white,
160
+ // so a literal white title disappeared into it.
161
+ color: colors.onPrimary,
99
162
  fontSize: "14px",
100
163
  fontWeight: "bold",
164
+ });
165
+ headerTitleRef = WidgetFactory({
166
+ tag: "span",
101
167
  textContent: text,
168
+ overflow: "hidden",
169
+ textOverflow: "ellipsis",
170
+ whiteSpace: "nowrap",
102
171
  });
172
+ headerRef.appendChild(headerTitleRef);
103
173
  container.insertBefore(headerRef, container.firstChild);
174
+ attachCopyButton();
104
175
  return headerRef;
105
176
  };
106
177
  ensureHeader(currentTitle);
@@ -110,10 +181,10 @@ code = "",
110
181
  tag: "div",
111
182
  backgroundColor,
112
183
  borderRadius: currentTitle
113
- ? `0 0 ${borderRadius}px ${borderRadius}px`
114
- : `${borderRadius}px`,
184
+ ? `0 0 ${toREM(borderRadius)} ${toREM(borderRadius)}`
185
+ : toREM(borderRadius),
115
186
  overflow: "auto",
116
- maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight,
187
+ maxHeight: toREM(maxHeight),
117
188
  overflowX: "auto",
118
189
  overflowY: "auto",
119
190
  });
@@ -132,22 +203,22 @@ code = "",
132
203
  tag: "div",
133
204
  backgroundColor,
134
205
  borderRight: `1px solid ${colors.border}`,
135
- padding: `${padding}px 0`,
206
+ padding: `${toREM(padding)} 0`,
136
207
  fontFamily: "monospace",
137
- fontSize: typeof fontSize === "number" ? `${fontSize}px` : fontSize,
208
+ fontSize: toREM(fontSize),
138
209
  lineHeight: "1.5",
139
210
  textAlign: "right",
140
- color: lineNumberColor,
211
+ color: gutterInk,
141
212
  userSelect: "none",
142
- width: `${lineNumberWidth}px`,
213
+ width: toREM(lineNumberWidth),
143
214
  flexShrink: 0,
144
215
  });
145
216
 
146
217
  codeColRef = WidgetFactory({
147
218
  tag: "div",
148
- padding: `${padding}px`,
219
+ padding: toREM(padding),
149
220
  fontFamily: "monospace",
150
- fontSize: typeof fontSize === "number" ? `${fontSize}px` : fontSize,
221
+ fontSize: toREM(fontSize),
151
222
  lineHeight: "1.5",
152
223
  whiteSpace: "pre",
153
224
  flex: 1,
@@ -170,9 +241,9 @@ code = "",
170
241
  } else {
171
242
  codeWrapperRef = WidgetFactory({
172
243
  tag: "div",
173
- padding: typeof padding === "number" ? `${padding}px` : padding,
244
+ padding: toREM(padding),
174
245
  fontFamily: "monospace",
175
- fontSize: typeof fontSize === "number" ? `${fontSize}px` : fontSize,
246
+ fontSize: toREM(fontSize),
176
247
  whiteSpace: "pre",
177
248
  lineHeight: "1.5",
178
249
  });
@@ -180,16 +251,18 @@ code = "",
180
251
  }
181
252
 
182
253
  container.appendChild(scrollWrapperRef);
254
+ if (!headerRef) attachCopyButton();
255
+ container.onUnmount(() => clearTimeout(copyResetTimer));
183
256
 
184
257
  // Fill initial content
185
258
  const lines = currentCode.split("\n");
186
- const highlightedLines = lines.map((line) => generateHighlightedHtml(line));
259
+ const highlightedLines = lines.map((line) => generateHighlightedHtml(line, syntaxPalette));
187
260
 
188
261
  if (showLineNumbers) {
189
262
  for (let i = 0; i < lines.length; i++) {
190
263
  const numberDiv = WidgetFactory({
191
264
  tag: "div",
192
- padding: `0 ${padding / 2}px 0 ${padding}px`,
265
+ padding: `0 ${toREM(padding / 2)} 0 ${toREM(padding)}`,
193
266
  whiteSpace: "pre",
194
267
  textContent: String(startingLineNumber + i),
195
268
  });
@@ -13,9 +13,21 @@ export const Container = (props = {}) => {
13
13
  overflow = "auto",
14
14
  boxSizing = "border-box",
15
15
  bgColor = colors.surface,
16
+ flex,
17
+ flexGrow,
18
+ flexShrink,
16
19
  ...rest
17
20
  } = props;
18
21
 
22
+ // Container paints `overflow: auto`, and a flex item whose overflow is not
23
+ // `visible` has an automatic minimum size of zero — so by default it shrank
24
+ // to nothing inside a column that overflows. Opting into the flex shorthand
25
+ // still wins, which is how a stretchable body keeps working.
26
+ const flexDefaults =
27
+ flex === undefined && flexGrow === undefined && flexShrink === undefined
28
+ ? { flexShrink: 0 }
29
+ : { flex, flexGrow, flexShrink };
30
+
19
31
  return WidgetFactory({
20
32
  tag: "div",
21
33
  widgetName: "Container",
@@ -24,6 +36,7 @@ export const Container = (props = {}) => {
24
36
  overflow: overflow,
25
37
  boxSizing: boxSizing,
26
38
  backgroundColor: bgColor,
39
+ ...flexDefaults,
27
40
  ...rest,
28
41
  });
29
42
  };
@@ -5,6 +5,8 @@
5
5
  import { WidgetFactory } from "../widget-factory/index.js";
6
6
  import { colors } from "../utils/themes.js";
7
7
 
8
+ import { toREM } from "../utils/units.js";
9
+
8
10
  export const DataTable = (props) => {
9
11
  const {
10
12
  columns,
@@ -22,7 +24,7 @@ export const DataTable = (props) => {
22
24
 
23
25
  rowBgColor = "transparent",
24
26
  rowTextColor = colors.text,
25
- rowFontSize = 13,
27
+ rowFontSize = 14,
26
28
 
27
29
  stripedRowBgColor = colors.gray50,
28
30
 
@@ -31,8 +33,12 @@ export const DataTable = (props) => {
31
33
  borderColor = colors.border,
32
34
  borderWidth = 1,
33
35
 
34
- cellPadding = "10px 12px",
35
- headerCellPadding = "12px",
36
+ // A padding the caller can hand as `8`, `"8px 12px"` or `"1rem"`: every
37
+ // write below goes through toREM, which scales the first two and leaves the
38
+ // third alone. Written straight to `style`, a bare number is an invalid CSS
39
+ // value the browser drops, and a string is a frozen pixel.
40
+ cellPadding = "8px 12px",
41
+ headerCellPadding = 12,
36
42
 
37
43
  // ========== OTHER PROPS ==========
38
44
  ...rest
@@ -61,7 +67,7 @@ export const DataTable = (props) => {
61
67
  for (const col of columns) {
62
68
  const th = document.createElement("th");
63
69
  th.textContent = typeof col === "string" ? col : col.label;
64
- th.style.padding = headerCellPadding;
70
+ th.style.padding = toREM(headerCellPadding);
65
71
  th.style.textAlign = "left";
66
72
  th.style.borderBottom = `2px solid ${borderColor}`;
67
73
  th.style.backgroundColor = headerBgColor;
@@ -69,7 +75,7 @@ export const DataTable = (props) => {
69
75
  th.style.fontWeight = headerFontWeight;
70
76
  th.style.fontSize =
71
77
  typeof headerFontSize === "number"
72
- ? `${headerFontSize}px`
78
+ ? toREM(headerFontSize)
73
79
  : headerFontSize;
74
80
 
75
81
  // Custom alignment per column
@@ -122,11 +128,11 @@ export const DataTable = (props) => {
122
128
  const value = row[key];
123
129
 
124
130
  td.textContent = value !== undefined ? String(value) : "";
125
- td.style.padding = cellPadding;
131
+ td.style.padding = toREM(cellPadding);
126
132
  td.style.borderBottom = `${borderWidth}px solid ${borderColor}`;
127
133
  td.style.color = rowTextColor;
128
134
  td.style.fontSize =
129
- typeof rowFontSize === "number" ? `${rowFontSize}px` : rowFontSize;
135
+ toREM(rowFontSize);
130
136
 
131
137
  // Custom alignment per column
132
138
  if (typeof col !== "string" && col.align) {
@@ -195,11 +201,11 @@ export const DataTable = (props) => {
195
201
  const value = row[key];
196
202
 
197
203
  td.textContent = value !== undefined ? String(value) : "";
198
- td.style.padding = cellPadding;
204
+ td.style.padding = toREM(cellPadding);
199
205
  td.style.borderBottom = `${borderWidth}px solid ${borderColor}`;
200
206
  td.style.color = rowTextColor;
201
207
  td.style.fontSize =
202
- typeof rowFontSize === "number" ? `${rowFontSize}px` : rowFontSize;
208
+ toREM(rowFontSize);
203
209
 
204
210
  if (typeof col !== "string" && col.align) {
205
211
  td.style.textAlign = col.align;
@@ -236,7 +242,7 @@ export const DataTable = (props) => {
236
242
  for (const col of newColumns) {
237
243
  const th = document.createElement("th");
238
244
  th.textContent = typeof col === "string" ? col : col.label;
239
- th.style.padding = headerCellPadding;
245
+ th.style.padding = toREM(headerCellPadding);
240
246
  th.style.textAlign = "left";
241
247
  th.style.borderBottom = `2px solid ${borderColor}`;
242
248
  th.style.backgroundColor = headerBgColor;
@@ -244,7 +250,7 @@ export const DataTable = (props) => {
244
250
  th.style.fontWeight = headerFontWeight;
245
251
  th.style.fontSize =
246
252
  typeof headerFontSize === "number"
247
- ? `${headerFontSize}px`
253
+ ? toREM(headerFontSize)
248
254
  : headerFontSize;
249
255
 
250
256
  if (typeof col !== "string" && col.align) {
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { WidgetFactory } from "../widget-factory/index.js";
6
6
  import { colors } from "../utils/themes.js";
7
+ import { toREM } from "../utils/units.js";
7
8
 
8
9
  export const DraggBox = (props) => {
9
10
  const {
@@ -53,7 +54,7 @@ export const DraggBox = (props) => {
53
54
  clone.style.zIndex = "99999";
54
55
  clone.style.backgroundColor = dragOverlayColor;
55
56
  clone.style.border = `2px dashed ${dragBorderColor}`;
56
- clone.style.borderRadius = "8px";
57
+ clone.style.borderRadius = toREM(8);
57
58
  document.body.appendChild(clone);
58
59
  return clone;
59
60
  };
@@ -5,11 +5,15 @@
5
5
  import { WidgetFactory } from "../widget-factory/index.js";
6
6
  import { colors } from "../utils/themes.js";
7
7
  import { composeUpdate } from "../utils/composeUpdate.js";
8
+ import { isReactive, reactiveKeyOf, resolveReactive } from "../widget-factory/reactiveValue.js";
9
+ import { registerWidget } from "../tools/useState.js";
8
10
  import { Container } from "./Container.js";
9
11
  import { Row } from "./Row.js";
10
12
  import { Column } from "./Column.js";
11
13
  import { Text } from "./Text.js";
12
14
  import { Icon } from "./Icon.js";
15
+ import { toREM } from "../utils/units.js";
16
+ import { TAP_MINIMUM } from "../tools/index.js";
13
17
 
14
18
  /**
15
19
  * Props owned by the Dropdown's closure state. Routed to the widget's `update`
@@ -22,7 +26,32 @@ const DROPDOWN_PROP_KEYS = [
22
26
  "width", "portal",
23
27
  ];
24
28
 
25
- export const Dropdown = (props) => {
29
+ /**
30
+ * The owned keys `update()` can actually apply, so they are also the ones worth
31
+ * subscribing. A callback is never bound: `onChange` has no branch in `update()`.
32
+ */
33
+ const BINDABLE_KEYS = [
34
+ "options", "value", "placeholder", "disabled", "label", "error", "variant",
35
+ "size", "borderRadius", "color", "bgColor", "textColor", "optionHoverColor",
36
+ "clearable",
37
+ ];
38
+
39
+ export const Dropdown = (inputProps = {}) => {
40
+ // The factory never receives these keys, so it can neither resolve a `useState`
41
+ // value for them (a live reactive is a *function*, and `findSelected()` would
42
+ // compare it against the option list and never match) nor subscribe this widget
43
+ // to the key. Both happen here instead: read once, keep the keys, bind after
44
+ // `composeUpdate` has routed a later patch into `update()`.
45
+ const boundKeys = new Map();
46
+ const props = { ...inputProps };
47
+ for (const key of BINDABLE_KEYS) {
48
+ const value = inputProps[key];
49
+ if (!isReactive(value)) continue;
50
+ props[key] = resolveReactive(value);
51
+ const stateKey = reactiveKeyOf(value);
52
+ if (stateKey) boundKeys.set(key, stateKey);
53
+ }
54
+
26
55
  const {
27
56
  options = [],
28
57
  value = null,
@@ -70,10 +99,12 @@ export const Dropdown = (props) => {
70
99
  let resizeListener = null;
71
100
  let scrollListener = null;
72
101
 
102
+ /** [vertical, horizontal] in framework units — see `Input`: a `"8px 14px"`
103
+ * string is raw CSS and stops the field growing with the reader's font. */
73
104
  const sizes = {
74
- small: { padding: "6px 12px", fontSize: 12 },
75
- medium: { padding: "8px 14px", fontSize: 14 },
76
- large: { padding: "12px 16px", fontSize: 16 },
105
+ small: { padding: [4, 12], fontSize: 12, minHeight: 32 },
106
+ medium: { padding: [8, 12], fontSize: 14, minHeight: TAP_MINIMUM },
107
+ large: { padding: [12, 16], fontSize: 16, minHeight: 48 },
77
108
  };
78
109
  let currentSize = sizes[size] || sizes.medium;
79
110
 
@@ -170,14 +201,16 @@ export const Dropdown = (props) => {
170
201
  alignItems: "center",
171
202
  justifyContent: "space-between",
172
203
  width: "100%",
173
- padding: currentSize.padding,
204
+ paddingVertical: currentSize.padding[0],
205
+ paddingHorizontal: currentSize.padding[1],
206
+ minHeight: currentSize.minHeight,
174
207
  backgroundColor:
175
208
  currentVariant === "filled" ? `${currentBgColor}CC` : "transparent",
176
209
  border:
177
210
  currentVariant === "outlined"
178
211
  ? `1px solid ${currentError ? colors.danger : colors.border}`
179
212
  : "none",
180
- borderRadius: `${currentBorderRadius}px`,
213
+ borderRadius: currentBorderRadius,
181
214
  cursor: currentDisabled ? "not-allowed" : "pointer",
182
215
  opacity: currentDisabled ? 0.5 : 1,
183
216
  boxSizing: "border-box",
@@ -209,11 +242,15 @@ export const Dropdown = (props) => {
209
242
  ],
210
243
  });
211
244
 
212
- // Main container
245
+ // The root must not paint a surface: only the selector does, and an
246
+ // `outlined` selector is deliberately transparent. Container's default
247
+ // background otherwise showed through behind the label as a full-width
248
+ // block, and painted the outlined field as if it were filled.
213
249
  const container = Container({
214
250
  widgetName: "Dropdown",
215
251
  position: "relative",
216
252
  width: width,
253
+ bgColor: "transparent",
217
254
  ...rest,
218
255
  });
219
256
 
@@ -221,9 +258,9 @@ export const Dropdown = (props) => {
221
258
  const labelEl = WidgetFactory({
222
259
  tag: "label",
223
260
  textContent: currentLabel,
224
- fontSize: "12px",
261
+ fontSize: 12,
225
262
  color: currentError ? colors.danger : colors.textSecondary,
226
- marginBottom: "4px",
263
+ marginBottom: 4,
227
264
  display: "block",
228
265
  });
229
266
  container.appendChild(labelEl);
@@ -291,7 +328,7 @@ export const Dropdown = (props) => {
291
328
  zIndex: 10000,
292
329
  border: `1px solid ${colors.border}`,
293
330
  backgroundColor: currentBgColor,
294
- borderRadius: `${currentBorderRadius}px`, // same borderRadius
331
+ borderRadius: currentBorderRadius, // same borderRadius
295
332
  boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
296
333
  maxHeight: "250px",
297
334
  overflowY: "auto",
@@ -321,12 +358,14 @@ export const Dropdown = (props) => {
321
358
  );
322
359
 
323
360
  const optionItem = Row({
324
- padding: "10px 16px",
361
+ paddingVertical: 8,
362
+ paddingHorizontal: 12,
363
+ minHeight: TAP_MINIMUM,
325
364
  cursor: "pointer",
326
365
  backgroundColor: isSelected ? currentColor + "15" : "transparent",
327
366
  alignItems: "center",
328
367
  justifyContent: "space-between",
329
- gap: "8px",
368
+ gap: 8,
330
369
  children: [
331
370
  Row({
332
371
  alignItems: "center",
@@ -469,8 +508,9 @@ export const Dropdown = (props) => {
469
508
  }
470
509
  if (needsDisplayUpdate) {
471
510
  updateDisplay();
472
- selector.style.padding = currentSize.padding;
473
- selector.style.borderRadius = `${currentBorderRadius}px`;
511
+ selector.style.padding = `${toREM(currentSize.padding[0])} ${toREM(currentSize.padding[1])}`;
512
+ selector.style.minHeight = toREM(currentSize.minHeight);
513
+ selector.style.borderRadius = toREM(currentBorderRadius);
474
514
  if (currentVariant === "outlined")
475
515
  selector.style.border = `1px solid ${currentError ? colors.danger : colors.border}`;
476
516
  else selector.style.border = "none";
@@ -499,6 +539,11 @@ export const Dropdown = (props) => {
499
539
  container.close = closeMenu;
500
540
  composeUpdate(container, DROPDOWN_PROP_KEYS, update);
501
541
 
542
+ // Bind after composing: `registerWidget` pushes the current store value through
543
+ // `update`, which must be the composed one that routes owned keys into this
544
+ // widget's closure. It also releases the subscription on unmount by itself.
545
+ for (const [propName, stateKey] of boundKeys) registerWidget(stateKey, container, propName);
546
+
502
547
  container.onUnmount(() => {
503
548
  closeMenu();
504
549
  });
@@ -5,6 +5,8 @@
5
5
  import { WidgetFactory } from "../widget-factory/index.js";
6
6
  import { colors } from "../utils/themes.js";
7
7
 
8
+ import { toREM } from "../utils/units.js";
9
+
8
10
  export const DroppBox = (props) => {
9
11
  const {
10
12
  child,
@@ -58,12 +60,12 @@ export const DroppBox = (props) => {
58
60
  transition: `all ${transitionDuration} ${transitionTiming}`,
59
61
  backgroundColor: bgColor,
60
62
  borderRadius:
61
- typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius,
63
+ toREM(borderRadius),
62
64
  borderWidth: `${borderWidth}px`,
63
65
  borderStyle: borderStyle,
64
66
  borderColor: borderColor,
65
67
  boxShadow: shadow,
66
- padding: typeof padding === "number" ? `${padding}px` : padding,
68
+ padding: toREM(padding),
67
69
  ...rest,
68
70
  });
69
71
 
@@ -7,6 +7,8 @@ import { Row } from "./Row.js";
7
7
  import { Text } from "./Text.js";
8
8
  import { Icon } from "./Icon.js";
9
9
  import { colors } from "../utils/themes.js";
10
+ import { toREM } from "../utils/units.js";
11
+ import { SHADOW_LEVELS, PILL } from "../tools/index.js";
10
12
 
11
13
  export const FloatingActionButton = (props) => {
12
14
  const {
@@ -14,9 +16,11 @@ export const FloatingActionButton = (props) => {
14
16
  label,
15
17
  onPress,
16
18
  backgroundColor = colors.primary,
17
- foregroundColor = "#ffffff",
18
- elevation = 6,
19
- borderRadius = 48,
19
+ foregroundColor = colors.onPrimary,
20
+ // Depth is the 0-5 scale, like every other widget; 6 was a number the scale
21
+ // does not have, which only worked because this widget built its own string.
22
+ elevation = 4,
23
+ borderRadius = PILL,
20
24
  padding = 12,
21
25
  mini = false,
22
26
  extended = false,
@@ -24,7 +28,9 @@ export const FloatingActionButton = (props) => {
24
28
  ...rest
25
29
  } = props;
26
30
 
27
- const size = mini ? 40 : 56;
31
+ // 48 and 40 are both scale steps, and both clear the Fitts floor: the mini FAB
32
+ // sits exactly on it, which is as small as a floating target may get.
33
+ const size = mini ? 40 : 48;
28
34
  const iconSize = mini ? 20 : 24;
29
35
 
30
36
  const children = [
@@ -58,14 +64,14 @@ export const FloatingActionButton = (props) => {
58
64
  gap: 8,
59
65
  width: extended ? "auto" : size,
60
66
  height: size,
61
- borderRadius: extended ? "24px" : "50%",
67
+ borderRadius: extended ? borderRadius : "50%",
62
68
  backgroundColor: backgroundColor,
63
69
  color: foregroundColor,
64
70
  cursor: disabled ? "not-allowed" : "pointer",
65
71
  opacity: disabled ? 0.6 : 1,
66
- boxShadow: `0 ${elevation / 2}px ${elevation}px rgba(0,0,0,0.3)`,
72
+ boxShadow: SHADOW_LEVELS[elevation] ?? SHADOW_LEVELS[4],
67
73
  transition: "all 0.2s ease",
68
- padding: extended ? "0 20px" : 0,
74
+ padding: extended ? `0 ${toREM(16)}` : 0,
69
75
  child: extended
70
76
  ? Row({ alignItems: "center", gap: 8, children })
71
77
  : children[0],