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
@@ -3,6 +3,8 @@
3
3
  * @description Button with size presets, variants, icons and gradient support.
4
4
  */
5
5
  import { WidgetFactory } from "../widget-factory/index.js";import { colors } from "../utils/themes.js";
6
+ import { SHADOW_LEVELS, TAP_MINIMUM } from "../tools/index.js";
7
+ import { readableInk } from "../utils/readableInk.js";
6
8
  import { Text } from "./Text.js";
7
9
  import { Icon } from "./Icon.js";
8
10
 
@@ -10,19 +12,20 @@ import { Icon } from "./Icon.js";
10
12
  // CONFIGURATION
11
13
  // ============================================================
12
14
 
15
+ /**
16
+ * Every number here is a framework unit — the factory converts lengths to `rem`,
17
+ * so the whole preset grows with the reader's font size instead of freezing at
18
+ * the pixel it was written in.
19
+ *
20
+ * `minHeight` is the Fitts floor: a pressable box the finger can hit, set on the
21
+ * box rather than reached through padding, so `small` stays a small *looking*
22
+ * control that is still a real target. `medium` and `large` land on 40 and 48;
23
+ * `small` is 32 and is a desktop-density escape hatch, not a touch target.
24
+ */
13
25
  const SIZE_PRESETS = {
14
- small: { padding: [6, 12], fontSize: 12, gap: 6, iconSize: 16 },
15
- medium: { padding: [10, 20], fontSize: 14, gap: 8, iconSize: 20 },
16
- large: { padding: [14, 28], fontSize: 16, gap: 10, iconSize: 24 },
17
- };
18
-
19
- const SHADOW_LEVELS = {
20
- 0: "none",
21
- 1: "0 1px 3px rgba(0,0,0,0.12)",
22
- 2: "0 3px 6px rgba(0,0,0,0.16)",
23
- 3: "0 6px 12px rgba(0,0,0,0.2)",
24
- 4: "0 10px 20px rgba(0,0,0,0.25)",
25
- 5: "0 15px 30px rgba(0,0,0,0.3)",
26
+ small: { padding: [4, 12], fontSize: 12, gap: 4, iconSize: 16, minHeight: 32 },
27
+ medium: { padding: [8, 16], fontSize: 14, gap: 8, iconSize: 20, minHeight: TAP_MINIMUM },
28
+ large: { padding: [12, 24], fontSize: 16, gap: 12, iconSize: 24, minHeight: 48 },
26
29
  };
27
30
 
28
31
  // ============================================================
@@ -42,20 +45,35 @@ function computeBackground(variant, disabled, bgColor, gradient) {
42
45
  return "transparent";
43
46
  }
44
47
 
45
- function computeTextColor(variant, disabled, color, bgColor) {
48
+ function computeTextColor(variant, disabled, color, background, inkFromFill) {
46
49
  if (disabled) return colors.textDisabled;
47
- if (variant === "filled") return color || "#fff";
48
- return color || bgColor || colors.primary;
50
+ if (variant === "filled") {
51
+ // The default `color` is the theme's ink, which is the right answer for a
52
+ // filled button sitting on the default surface. Once the caller names the
53
+ // fill, that ink can be the fill itself (dark on the light theme's primary,
54
+ // near-white on the dark theme's), so the label has to follow the plate.
55
+ if (!inkFromFill) return color;
56
+ if (background === colors.primary) return colors.onPrimary;
57
+ if (background === colors.secondary) return colors.onSecondary;
58
+ return readableInk(background, colors.text);
59
+ }
60
+ return color || background || colors.primary;
49
61
  }
50
62
 
51
- function computeBorder(variant, disabled, color, bgColor) {
63
+ function computeBorder(variant, disabled, color, bgColor, borderColor, namesItsOwnFill) {
52
64
  if (disabled) return `1px solid ${colors.gray300}`;
53
65
  if (variant === "outlined") {
54
- const borderColor = color || bgColor || colors.primary;
55
- return `2px solid ${borderColor}`;
66
+ const tint = color || bgColor || colors.primary;
67
+ return `2px solid ${tint}`;
56
68
  }
57
69
  if (variant === "text") return "none";
58
- return `1px solid ${colors.border}`;
70
+ // A filled button whose caller named the fill is a colour decision; a neutral
71
+ // grey ring around it is an edge that belongs to no token and reads as a
72
+ // rendering bug on the deep variants. A button that names no fill lands on
73
+ // `colors.surface`, and there the hairline is what keeps the control findable
74
+ // on a card of the same colour. An explicit `borderColor` always wins.
75
+ if (borderColor) return `1px solid ${borderColor}`;
76
+ return namesItsOwnFill ? "none" : `1px solid ${colors.border}`;
59
77
  }
60
78
 
61
79
  function createIcon(name, size, color) {
@@ -120,11 +138,8 @@ function buildButtonChildren(text, textColor, fontSize, gap, iconElements) {
120
138
  return children;
121
139
  }
122
140
 
123
- function applySpacingToProps(buttonProps, padding, margin) {
124
- const normalizedPadding = normalizeSpacing(
125
- padding,
126
- SIZE_PRESETS.medium.padding,
127
- );
141
+ function applySpacingToProps(buttonProps, padding, margin, fallbackPadding) {
142
+ const normalizedPadding = normalizeSpacing(padding, fallbackPadding);
128
143
  const normalizedMargin = normalizeSpacing(margin, undefined);
129
144
 
130
145
  if (Array.isArray(normalizedPadding)) {
@@ -165,8 +180,8 @@ export const Button = (props) => {
165
180
  iconPosition = "left",
166
181
  variant = "filled",
167
182
  size = "medium",
168
- borderRadius = 24,
169
- elevation = 2,
183
+ borderRadius = 16,
184
+ elevation = null,
170
185
  disabled = false,
171
186
  fullWidth = false,
172
187
  bgColor = colors.surface,
@@ -175,11 +190,34 @@ export const Button = (props) => {
175
190
  } = props;
176
191
 
177
192
  const sizePreset = SIZE_PRESETS[size] || SIZE_PRESETS.medium;
178
- const shadow = SHADOW_LEVELS[elevation] || SHADOW_LEVELS[0];
193
+ // Depth follows the variant: a text or outlined button is not a surface lifted
194
+ // off the page, and a shadow drawn around a transparent box paints a grey blob
195
+ // where the label should sit alone. An explicit `elevation` still wins.
196
+ const lifted = elevation === null ? (variant === "filled" ? 2 : 0) : elevation;
197
+ const shadow = SHADOW_LEVELS[lifted] || SHADOW_LEVELS[0];
179
198
 
180
199
  const background = computeBackground(variant, disabled, bgColor, gradient);
181
- const textColor = computeTextColor(variant, disabled, color, bgColor);
182
- const border = computeBorder(variant, disabled, color, bgColor);
200
+ // `bgColor` above always has a value (the default is the surface), so whether
201
+ // the caller chose the fill has to be read off the props, not the local.
202
+ const namesItsOwnFill =
203
+ props.bgColor !== undefined ||
204
+ props.backgroundColor !== undefined ||
205
+ !!gradient;
206
+ const textColor = computeTextColor(
207
+ variant,
208
+ disabled,
209
+ color,
210
+ background,
211
+ props.color === undefined && namesItsOwnFill,
212
+ );
213
+ const border = computeBorder(
214
+ variant,
215
+ disabled,
216
+ color,
217
+ bgColor,
218
+ rest.borderColor,
219
+ namesItsOwnFill,
220
+ );
183
221
 
184
222
  const iconNames = resolveIconNames({
185
223
  icon,
@@ -216,6 +254,7 @@ export const Button = (props) => {
216
254
  justifyContent: "center",
217
255
  gap: sizePreset.gap,
218
256
  borderRadius,
257
+ minHeight: sizePreset.minHeight,
219
258
  width: fullWidth ? "100%" : "auto",
220
259
  backgroundColor: background,
221
260
  border,
@@ -231,7 +270,7 @@ export const Button = (props) => {
231
270
  ...rest,
232
271
  };
233
272
 
234
- applySpacingToProps(buttonProps, padding, margin);
273
+ applySpacingToProps(buttonProps, padding, margin, sizePreset.padding);
235
274
 
236
275
  if (gradient) {
237
276
  buttonProps.background = gradient;
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { WidgetFactory } from "../widget-factory/index.js";
6
6
  import { colors } from "../utils/themes.js";
7
- import { border } from "../tools/index.js";
7
+ import { border, SHADOW_LEVELS } from "../tools/index.js";
8
8
 
9
9
  export const Card = (props = {}) => {
10
10
  const {
@@ -24,7 +24,11 @@ export const Card = (props = {}) => {
24
24
  backgroundColor: bgColor,
25
25
  borderRadius: borderRadius,
26
26
  padding: padding,
27
- elevation: elevation,
27
+ // The engine routes `elevation` straight into `style.boxShadow`, and CSS drops
28
+ // a bare number: forwarding it left every card flat, whatever depth was asked
29
+ // for. Depth is a scale, so the level is read here into a real shadow — and an
30
+ // explicit `shadow`/`boxShadow` in `rest` still wins over the scale.
31
+ boxShadow: SHADOW_LEVELS[elevation] ?? SHADOW_LEVELS[2],
28
32
  border: showBorder ? border(borderWidth, borderStyle, borderColor) : "none",
29
33
  ...rest,
30
34
  });
@@ -6,6 +6,8 @@ import { WidgetFactory } from "../widget-factory/index.js";
6
6
  import { Icon } from "./Icon.js";
7
7
  import { colors } from "../utils/themes.js";
8
8
 
9
+ import { toREM } from "../utils/units.js";
10
+
9
11
  export const Carousel = (props) => {
10
12
  const {
11
13
  items = [],
@@ -59,12 +61,12 @@ export const Carousel = (props) => {
59
61
  dot.style.backgroundColor = dotActiveColor;
60
62
  dot.style.width =
61
63
  typeof dotActiveSize === "number"
62
- ? `${dotActiveSize}px`
64
+ ? toREM(dotActiveSize)
63
65
  : dotActiveSize;
64
66
  } else {
65
67
  dot.style.backgroundColor = dotColor;
66
68
  dot.style.width =
67
- typeof dotSize === "number" ? `${dotSize}px` : dotSize;
69
+ toREM(dotSize);
68
70
  }
69
71
  }
70
72
  };
@@ -154,8 +156,8 @@ export const Carousel = (props) => {
154
156
  cursor: "pointer",
155
157
  backgroundColor: buttonBgColor,
156
158
  borderRadius: "50%",
157
- width: typeof buttonSize === "number" ? `${buttonSize}px` : buttonSize,
158
- height: typeof buttonSize === "number" ? `${buttonSize}px` : buttonSize,
159
+ width: toREM(buttonSize),
160
+ height: toREM(buttonSize),
159
161
  display: "flex",
160
162
  alignItems: "center",
161
163
  justifyContent: "center",
@@ -173,11 +175,11 @@ export const Carousel = (props) => {
173
175
  const container = WidgetFactory({
174
176
  widgetName: "Carousel",
175
177
  position: "relative",
176
- width: typeof width === "number" ? `${width}px` : width,
177
- height: typeof height === "number" ? `${height}px` : height,
178
+ width: toREM(width),
179
+ height: toREM(height),
178
180
  overflow: "hidden",
179
181
  borderRadius:
180
- typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius,
182
+ toREM(borderRadius),
181
183
  ...rest,
182
184
  });
183
185
 
@@ -255,12 +257,12 @@ export const Carousel = (props) => {
255
257
  width:
256
258
  index === currentIndex
257
259
  ? typeof dotActiveSize === "number"
258
- ? `${dotActiveSize}px`
260
+ ? toREM(dotActiveSize)
259
261
  : dotActiveSize
260
262
  : typeof dotSize === "number"
261
- ? `${dotSize}px`
263
+ ? toREM(dotSize)
262
264
  : dotSize,
263
- height: typeof dotSize === "number" ? `${dotSize}px` : dotSize,
265
+ height: toREM(dotSize),
264
266
  borderRadius: "4px",
265
267
  backgroundColor: index === currentIndex ? dotActiveColor : dotColor,
266
268
  cursor: "pointer",
@@ -42,6 +42,8 @@ import { colors } from "../utils/themes.js";
42
42
  * @param {number} [props.borderRadius=8] - Corner radius of the chart container in pixels.
43
43
  * @returns {HTMLElement} The chart container element, augmented with `updateData` and `redraw` methods.
44
44
  */
45
+ import { toREM } from "../utils/units.js";
46
+
45
47
  export const Chart = (props) => {
46
48
  const {
47
49
  type = "candle",
@@ -130,6 +132,19 @@ export const Chart = (props) => {
130
132
  maxVal = Math.max(...data);
131
133
  minVal = Math.min(...data);
132
134
  }
135
+ // Bars are measured from zero: if the scale started at the smallest
136
+ // value instead, `getYPosition(0)` fell below the axis and every bar
137
+ // ran past the baseline to the canvas edge, so the bottom label (6)
138
+ // did not describe where the bars started.
139
+ if (type === "bar") {
140
+ maxVal = Math.max(0, maxVal);
141
+ minVal = Math.min(0, minVal);
142
+ const barRange = maxVal - minVal || 1;
143
+ return {
144
+ maxValue: maxVal + (maxVal > 0 ? barRange * 0.1 : 0),
145
+ minValue: minVal - (minVal < 0 ? barRange * 0.1 : 0),
146
+ };
147
+ }
133
148
  const range = maxVal - minVal;
134
149
  const paddingRange = range * 0.1;
135
150
  return {
@@ -170,8 +185,11 @@ export const Chart = (props) => {
170
185
  */
171
186
  const drawBar = (ctx, canvasWidth, chartHeight, maxValue, minValue) => {
172
187
  const availableWidth = canvasWidth - pad.left - pad.right;
173
- const barWidth = (availableWidth / data.length) * 0.7;
174
- const barSpacing = (availableWidth / data.length) * 0.3;
188
+ const bandWidth = availableWidth / data.length;
189
+ // 70/30 left a gap 43% as wide as the bar; on a wide chart the columns
190
+ // read as unrelated blocks instead of one series.
191
+ const barWidth = bandWidth * 0.8;
192
+ const barSpacing = bandWidth - barWidth;
175
193
 
176
194
  data.forEach((value, index) => {
177
195
  const x = pad.left + index * (barWidth + barSpacing) + barSpacing / 2;
@@ -186,6 +204,11 @@ export const Chart = (props) => {
186
204
  ctx.fillStyle = textColor;
187
205
  ctx.font = "8px sans-serif";
188
206
  ctx.fillText(value, x + barWidth / 2 - 5, y - 2);
207
+ }
208
+ // Bars are data points too: `showLabels` used to be ignored here
209
+ // unless `showValues` was on, so the documented default drew no
210
+ // x-axis labels at all.
211
+ if (showLabels && labels[index]) {
189
212
  ctx.save();
190
213
  ctx.translate(x + barWidth / 2, chartHeight - pad.bottom + 8);
191
214
  ctx.rotate(-0.3);
@@ -525,11 +548,11 @@ export const Chart = (props) => {
525
548
 
526
549
  const container = WidgetFactory({
527
550
  widgetName: "Chart",
528
- width: typeof width === "number" ? `${width}px` : width,
529
- height: typeof height === "number" ? `${height}px` : height,
551
+ width: toREM(width),
552
+ height: toREM(height),
530
553
  backgroundColor: bgColor,
531
554
  borderRadius:
532
- typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius,
555
+ toREM(borderRadius),
533
556
  overflow: "hidden",
534
557
  display: "flex",
535
558
  flexDirection: "column",
@@ -3,7 +3,9 @@
3
3
  * @description Checkbox input with controlled/uncontrolled state.
4
4
  */
5
5
  import { WidgetFactory } from "../widget-factory/index.js";
6
+ import { Text } from "./Text.js";
6
7
  import { colors } from "../utils/themes.js";
8
+ import { TAP_MINIMUM, LABEL_TEXT } from "../tools/index.js";
7
9
 
8
10
  export const Checkbox = (props) => {
9
11
  const {
@@ -13,6 +15,7 @@ export const Checkbox = (props) => {
13
15
  disabled = false,
14
16
  size = 20,
15
17
  activeColor = colors.primary,
18
+ label = null,
16
19
  ...rest
17
20
  } = props;
18
21
 
@@ -24,45 +27,100 @@ export const Checkbox = (props) => {
24
27
  let checkboxRef = null;
25
28
  let checkMarkRef = null;
26
29
 
30
+ // The box's "off" rim is a control outline, not a hairline: `colors.border`
31
+ // measured 1.2:1 on the dark card and 1.3:1 on white, so an unchecked box was
32
+ // invisible in both themes. `gray500` is the same #71717A in both palettes and
33
+ // clears the 3:1 non-text minimum on each surface.
34
+ const offColor = disabled ? colors.border : colors.gray500;
35
+
27
36
  const updateUI = (newChecked) => {
28
37
  if (checkboxRef) {
29
- checkboxRef.style.backgroundColor = newChecked ? activeColor : "transparent";
30
- checkboxRef.style.border = `2px solid ${newChecked ? activeColor : colors.border}`;
38
+ checkboxRef.style.border = `2px solid ${newChecked ? activeColor : offColor}`;
31
39
  }
32
40
  if (checkMarkRef) {
33
41
  checkMarkRef.style.display = newChecked ? "inline-block" : "none";
34
42
  }
35
43
  };
36
44
 
37
- // Createste checkmark
45
+ // The tick is drawn inside the frame, in the active colour: a filled plate
46
+ // would hide the border the box is supposed to show, and the theme ink it used
47
+ // to carry (white in light, near-black in dark) only read on that plate.
38
48
  const checkMark = WidgetFactory({
39
49
  tag: "span",
40
50
  textContent: "✓",
41
- color: "#fff",
42
- fontSize: size * 0.7,
51
+ color: disabled ? colors.textDisabled : activeColor,
52
+ fontSize: Math.max(12, Math.round(size * 0.65)),
43
53
  fontWeight: "bold",
54
+ lineHeight: 1,
44
55
  display: internalChecked ? "inline-block" : "none",
45
56
  });
46
57
  checkMarkRef = checkMark;
47
58
 
48
- // Createste main checkbox container
49
- const checkbox = WidgetFactory({
50
- widgetName: "Checkbox",
59
+ // The visual square. Everything drawn lives here, so the box below can be the
60
+ // finger's target without becoming a 40-unit checkbox.
61
+ const box = WidgetFactory({
51
62
  width: size,
52
63
  height: size,
53
- border: `2px solid ${disabled ? colors.border : internalChecked ? activeColor : colors.border}`,
54
- borderRadius: 6,
55
- backgroundColor: internalChecked ? activeColor : "transparent",
64
+ border: `2px solid ${disabled ? colors.border : internalChecked ? activeColor : offColor}`,
65
+ borderRadius: 0,
66
+ backgroundColor: "transparent",
56
67
  display: "flex",
57
68
  alignItems: "center",
58
69
  justifyContent: "center",
59
70
  transition: "all 0.2s ease",
60
- cursor: disabled ? "not-allowed" : "pointer",
61
- opacity: disabled ? 0.5 : 1,
71
+ boxSizing: "border-box",
62
72
  child: checkMark,
63
- ...rest,
64
73
  });
65
- checkboxRef = checkbox;
74
+ checkboxRef = box;
75
+
76
+ // Createste main checkbox container
77
+ //
78
+ // The overhang is given back with a negative margin. A 40-unit box in the
79
+ // layout next to a label stretched the row to 40 and left the text at the top
80
+ // of it, so the box sat ~10 units below the words and the requested `gap` was
81
+ // silently doubled. The press still lands on the full 40-unit box, which now
82
+ // overflows its layout footprint instead of pushing the row around it.
83
+ const overhang = Math.max(0, (TAP_MINIMUM - size) / 2);
84
+
85
+ // A `label` is part of the control, not a caption beside it: the whole row is
86
+ // pressable. It also cannot reach the factory as a flat prop, which would set
87
+ // the root's `textContent` and wipe the box it is supposed to describe.
88
+ const isLabelled = label !== null && label !== undefined && label !== "";
89
+
90
+ const rootProps = {
91
+ widgetName: "Checkbox",
92
+ display: "inline-flex",
93
+ alignItems: "center",
94
+ justifyContent: isLabelled ? "flex-start" : "center",
95
+ gap: isLabelled ? 12 : undefined,
96
+ minWidth: isLabelled ? undefined : TAP_MINIMUM,
97
+ minHeight: TAP_MINIMUM,
98
+ margin: isLabelled ? 0 : -overhang,
99
+ flexShrink: 0,
100
+ cursor: disabled ? "not-allowed" : "pointer",
101
+ opacity: disabled ? 0.5 : 1,
102
+ child: isLabelled
103
+ ? [
104
+ box,
105
+ Text({
106
+ text: label,
107
+ size: LABEL_TEXT,
108
+ fontWeight: 500,
109
+ color: disabled ? colors.textDisabled : colors.text,
110
+ }),
111
+ ]
112
+ : box,
113
+ };
114
+
115
+ // Pressing the words must not scale the words. The box on its own keeps the
116
+ // default press feedback, because there the root *is* the control.
117
+ if (isLabelled) rootProps.disableTransform = true;
118
+
119
+ const checkbox = WidgetFactory({ ...rootProps, ...rest });
120
+
121
+ // Inline usage (a checkbox dropped mid-paragraph) would otherwise
122
+ // baseline-align the box's bottom edge and lift the square above the text.
123
+ checkbox.style.verticalAlign = "middle";
66
124
 
67
125
  // Handle click
68
126
  if (!disabled) {
@@ -80,19 +138,19 @@ export const Checkbox = (props) => {
80
138
  // padding…) calls update — leaving the control unable to toggle ever again.
81
139
  checkbox.update({ onPress: handleClick });
82
140
 
83
- // Hover effect
141
+ // Hover effect — on the drawn square, not the whole labelled row.
84
142
  checkbox.addEventListener("mouseenter", () => {
85
143
  if (!disabled && !internalChecked) {
86
144
  // `color-mix` rather than `activeColor + "20"`: that string concat only
87
145
  // produced a valid color for `#rrggbb` hexes and silently broke on
88
146
  // `rgba()`, named colors and 3-digit hex `activeColor` values.
89
- checkbox.style.backgroundColor = `color-mix(in srgb, ${activeColor} 12%, transparent)`;
147
+ box.style.backgroundColor = `color-mix(in srgb, ${activeColor} 12%, transparent)`;
90
148
  }
91
149
  });
92
150
 
93
151
  checkbox.addEventListener("mouseleave", () => {
94
152
  if (!disabled && !internalChecked) {
95
- checkbox.style.backgroundColor = "transparent";
153
+ box.style.backgroundColor = "transparent";
96
154
  }
97
155
  });
98
156
  }
@@ -10,6 +10,7 @@ import { Row } from "./Row.js";
10
10
  import { Text } from "./Text.js";
11
11
  import { Icon } from "./Icon.js";
12
12
  import { colors } from "../utils/themes.js";
13
+ import { PILL, TAP_MINIMUM } from "../tools/index.js";
13
14
 
14
15
  /**
15
16
  * Creates a Chip widget — a small interactive label commonly used for tags,
@@ -22,10 +23,10 @@ import { colors } from "../utils/themes.js";
22
23
  * @param {Function} [props.onDelete] - When provided, renders a close ("×") icon on the right side of the chip. Called when that icon is clicked.
23
24
  * @param {'filled'|'outlined'} [props.variant='filled'] - Visual style. 'filled' uses a solid background; 'outlined' uses a transparent background with a border.
24
25
  * @param {string} [props.color=colors.primary] - Chip accent color: used as the background (filled) or border/text (outlined).
25
- * @param {string} [props.textColor] - Explicit text color override. Defaults to '#fff' (filled) or `color` (outlined).
26
+ * @param {string} [props.textColor] - Explicit text color override. Defaults to `colors.onPrimary` (filled) or `color` (outlined).
26
27
  * @param {string} [props.borderColor] - Explicit border color override (outlined variant only). Defaults to `color`.
27
- * @param {number} [props.borderRadius=32] - Corner radius in pixels. High values produce a pill shape.
28
- * @param {string} [props.padding='4px 12px'] - Internal padding of the chip row.
28
+ * @param {number} [props.borderRadius=999] - Corner radius in framework units (converted to `rem`). The default clamps to a pill.
29
+ * @param {number|string} [props.padding] - Internal padding. A number is a uniform gutter (doubled horizontally); omitted it is 4 vertical x 12 horizontal, in framework units.
29
30
  * @param {number} [props.gap=4] - Gap in pixels between icon, label, and delete icon.
30
31
  * @param {number} [props.size=12] - Font size in pixels. Also scales the icons proportionally.
31
32
  * @param {number} [props.elevation=0] - Box-shadow depth. 0 means no shadow.
@@ -41,14 +42,26 @@ export const Chip = (props) => {
41
42
  color = colors.primary,
42
43
  textColor,
43
44
  borderColor,
44
- borderRadius = 32,
45
- padding = "4px 12px",
45
+ borderRadius = PILL,
46
+ padding,
46
47
  gap = 4,
47
48
  size = 12,
48
49
  elevation = 0,
49
50
  ...rest
50
51
  } = props;
51
52
 
53
+ /**
54
+ * `4px 12px` as a literal string reaches the factory as raw CSS and freezes
55
+ * there: the chip's type grew with the reader's font and its gutters did not.
56
+ * Numbers go through `toREM` instead, and the pair is on the spacing scale.
57
+ */
58
+ const chipPadding =
59
+ padding === undefined
60
+ ? { paddingVertical: 4, paddingHorizontal: 12 }
61
+ : typeof padding === "number"
62
+ ? { paddingVertical: padding, paddingHorizontal: padding * 2 }
63
+ : { padding };
64
+
52
65
  // Derive background, text, and border colors from the variant
53
66
  let bgColor, txtColor, brdColor;
54
67
 
@@ -58,7 +71,10 @@ export const Chip = (props) => {
58
71
  brdColor = borderColor || color;
59
72
  } else {
60
73
  bgColor = color;
61
- txtColor = textColor || "#fff";
74
+ // The label sits on the primary plate, so its ink is the theme's, not a
75
+ // literal white: in dark mode a primary chip is near-white and white words
76
+ // would vanish.
77
+ txtColor = textColor || colors.onPrimary;
62
78
  brdColor = "transparent";
63
79
  }
64
80
 
@@ -88,7 +104,7 @@ export const Chip = (props) => {
88
104
  if (onDelete) {
89
105
  const closeIcon = Icon({
90
106
  name: "close",
91
- size: size - 2,
107
+ size: Math.max(12, size - 2),
92
108
  color: txtColor,
93
109
  cursor: "pointer",
94
110
  onclick: (e) => {
@@ -106,7 +122,7 @@ export const Chip = (props) => {
106
122
  backgroundColor: bgColor,
107
123
  border: variant === "outlined" ? `1px solid ${brdColor}` : "none",
108
124
  borderRadius: borderRadius,
109
- padding: padding,
125
+ ...chipPadding,
110
126
  width: "fit-content",
111
127
  minWidth: "auto",
112
128
  boxShadow:
@@ -116,10 +132,15 @@ export const Chip = (props) => {
116
132
  children: children,
117
133
  });
118
134
 
119
- // Outer wrapper element enables the onclick without styling conflicts
135
+ // Outer wrapper element enables the onclick without styling conflicts.
136
+ // A chip you can press or dismiss is a target, so the wrapper carries the
137
+ // Fitts floor while the pill inside keeps its compact 23-unit height.
120
138
  const chip = WidgetFactory({
121
139
  widgetName: "Chip",
122
- display: "inline-block",
140
+ display: "inline-flex",
141
+ alignItems: "center",
142
+ justifyContent: "center",
143
+ ...(onPress || onDelete ? { minHeight: TAP_MINIMUM } : {}),
123
144
  cursor: onPress ? "pointer" : "default",
124
145
  transition: "all 0.2s ease",
125
146
  child: chipContent,
@@ -85,13 +85,13 @@ export const CircularBar = (props) => {
85
85
 
86
86
  const updateCanvasSize = () => {
87
87
  if (!canvas) return;
88
- let width;
89
- if (isFixedSize) width = fixedSize;
90
- else {
91
- const rect = container.getBoundingClientRect();
92
- width = rect.width;
93
- if (width <= 0) width = 200;
94
- }
88
+ // The box is routed through rem, so a fixed `size: 120` paints 180px wide
89
+ // when the reader's base font is 24. The drawing buffer has to follow that
90
+ // rendered box, not the number the developer wrote, or the ring grows and
91
+ // turns soft. The number is only the fallback for a node with no layout yet.
92
+ const measured = container.getBoundingClientRect().width;
93
+ const width =
94
+ measured > 0 ? measured : isFixedSize ? fixedSize : 200;
95
95
  canvas.width = width;
96
96
  canvas.height = width;
97
97
  currentWidth = width;