flet-box 1.0.7 → 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 (136) hide show
  1. package/bin/cli.js +13 -0
  2. package/bin/commands/checkApi.js +13 -1
  3. package/bin/commands/checkUi.js +1369 -0
  4. package/bin/commands/create.js +49 -5
  5. package/bin/commands/createBundle.js +21 -0
  6. package/bin/commands/runServer.js +11 -2
  7. package/bin/utils/pwaShell.js +137 -0
  8. package/bin/utils/templates.js +1373 -434
  9. package/docs/README.md +2 -0
  10. package/docs/cli/README.md +62 -1
  11. package/docs/guides/README.md +2 -0
  12. package/docs/guides/clean-ui.md +211 -0
  13. package/docs/guides/minification-and-protection.md +1 -1
  14. package/docs/guides/online-updates.md +48 -0
  15. package/docs/tools/README.md +1 -1
  16. package/docs/tools/animation.md +1 -1
  17. package/docs/tools/markdown.md +3 -3
  18. package/docs/tools/theme.md +41 -1
  19. package/docs/widget/Accordion.md +3 -0
  20. package/docs/widget/AdMob.md +4 -1
  21. package/docs/widget/AdSense.md +3 -0
  22. package/docs/widget/AnimatedBox.md +6 -3
  23. package/docs/widget/AnimatedText.md +3 -0
  24. package/docs/widget/Audio.md +3 -0
  25. package/docs/widget/Avatar.md +3 -0
  26. package/docs/widget/Badge.md +3 -0
  27. package/docs/widget/Button.md +3 -0
  28. package/docs/widget/COMMON_PROPS.md +52 -0
  29. package/docs/widget/Card.md +3 -0
  30. package/docs/widget/Carousel.md +3 -0
  31. package/docs/widget/Chart.md +3 -0
  32. package/docs/widget/Checkbox.md +3 -0
  33. package/docs/widget/Chip.md +3 -0
  34. package/docs/widget/CircularBar.md +3 -0
  35. package/docs/widget/CircularChart.md +3 -0
  36. package/docs/widget/CodeViewer.md +3 -0
  37. package/docs/widget/Column.md +3 -0
  38. package/docs/widget/Container.md +3 -0
  39. package/docs/widget/DataTable.md +3 -0
  40. package/docs/widget/Divider.md +3 -0
  41. package/docs/widget/DraggBox.md +3 -0
  42. package/docs/widget/Dropdown.md +3 -0
  43. package/docs/widget/DroppBox.md +3 -0
  44. package/docs/widget/FloatingActionButton.md +3 -0
  45. package/docs/widget/GridView.md +3 -0
  46. package/docs/widget/Icon.md +3 -0
  47. package/docs/widget/Image.md +3 -0
  48. package/docs/widget/Input.md +3 -0
  49. package/docs/widget/Inspector.md +2 -2
  50. package/docs/widget/InstallButton.md +3 -0
  51. package/docs/widget/ListTile.md +3 -0
  52. package/docs/widget/ListView.md +3 -0
  53. package/docs/widget/Maps.md +3 -0
  54. package/docs/widget/Markdown.md +3 -0
  55. package/docs/widget/MatrixRain.md +3 -0
  56. package/docs/widget/Pagination.md +3 -0
  57. package/docs/widget/ParallaxBox.md +3 -0
  58. package/docs/widget/ProgressBar.md +3 -0
  59. package/docs/widget/QRCode.md +3 -0
  60. package/docs/widget/Radio.md +4 -1
  61. package/docs/widget/Rating.md +3 -0
  62. package/docs/widget/Row.md +3 -0
  63. package/docs/widget/Skeleton.md +3 -0
  64. package/docs/widget/Slider.md +3 -0
  65. package/docs/widget/Stack.md +3 -0
  66. package/docs/widget/Stepper.md +3 -0
  67. package/docs/widget/Switch.md +3 -0
  68. package/docs/widget/Tabs.md +5 -5
  69. package/docs/widget/Text.md +3 -0
  70. package/docs/widget/Tooltip.md +3 -0
  71. package/docs/widget/TreeView.md +3 -0
  72. package/docs/widget/Video.md +3 -0
  73. package/package.json +2 -2
  74. package/src/animations/AnimatedBox.js +4 -1
  75. package/src/animations/MatrixRain.js +3 -3
  76. package/src/index.d.ts +64 -8
  77. package/src/index.js +1 -0
  78. package/src/navigations/AppBar.js +7 -12
  79. package/src/navigations/BottomNavigation.js +8 -13
  80. package/src/navigations/CollapsibleSideBar.js +30 -8
  81. package/src/navigations/Drawer.js +16 -14
  82. package/src/navigations/DrawerItem.js +3 -1
  83. package/src/navigations/Scaffold.js +26 -12
  84. package/src/navigations/Tabs.js +104 -59
  85. package/src/tools/color.js +54 -0
  86. package/src/tools/index.js +9 -1
  87. package/src/tools/scale.js +41 -0
  88. package/src/tools/shadow.js +20 -0
  89. package/src/utils/index.js +1 -0
  90. package/src/utils/readableInk.js +19 -0
  91. package/src/utils/syntaxHighlight.js +78 -10
  92. package/src/utils/themes.js +217 -54
  93. package/src/utils/units.js +33 -11
  94. package/src/widget-factory/effects.js +56 -5
  95. package/src/widget-factory/processProps.js +10 -1
  96. package/src/widget-factory/remTool.js +15 -29
  97. package/src/widget-factory/stackPosition.js +18 -3
  98. package/src/widget-factory/translateProps.js +28 -0
  99. package/src/widgets/Accordion.js +20 -23
  100. package/src/widgets/AdMob.js +7 -2
  101. package/src/widgets/Avatar.js +18 -12
  102. package/src/widgets/Badge.js +3 -2
  103. package/src/widgets/BottomSheet.js +14 -13
  104. package/src/widgets/Button.js +69 -30
  105. package/src/widgets/Card.js +6 -2
  106. package/src/widgets/Carousel.js +12 -10
  107. package/src/widgets/Chart.js +28 -5
  108. package/src/widgets/Checkbox.js +76 -18
  109. package/src/widgets/Chip.js +31 -10
  110. package/src/widgets/CircularBar.js +7 -7
  111. package/src/widgets/CircularChart.js +26 -8
  112. package/src/widgets/CodeViewer.js +93 -20
  113. package/src/widgets/DataTable.js +17 -11
  114. package/src/widgets/DraggBox.js +2 -1
  115. package/src/widgets/Dropdown.js +26 -13
  116. package/src/widgets/DroppBox.js +4 -2
  117. package/src/widgets/FloatingActionButton.js +13 -7
  118. package/src/widgets/Icon.js +3 -1
  119. package/src/widgets/Input.js +30 -23
  120. package/src/widgets/Inspector.js +54 -1
  121. package/src/widgets/InstallButton.js +6 -4
  122. package/src/widgets/ListTile.js +4 -3
  123. package/src/widgets/ListView.js +30 -12
  124. package/src/widgets/Markdown.js +37 -26
  125. package/src/widgets/Modal.js +14 -11
  126. package/src/widgets/Pagination.js +16 -11
  127. package/src/widgets/ProgressBar.js +6 -4
  128. package/src/widgets/Radio.js +73 -15
  129. package/src/widgets/Rating.js +24 -6
  130. package/src/widgets/Skeleton.js +31 -31
  131. package/src/widgets/Slider.js +29 -18
  132. package/src/widgets/SnackBar.js +35 -39
  133. package/src/widgets/Stepper.js +13 -9
  134. package/src/widgets/Switch.js +59 -18
  135. package/src/widgets/Tooltip.js +12 -10
  136. package/src/widgets/TreeView.js +27 -15
@@ -6,6 +6,8 @@ import { WidgetFactory } from "../widget-factory/index.js";
6
6
  import { colors } from "../utils/themes.js";
7
7
  import { injectKeyframes } from "../utils/styleInjector.js";
8
8
 
9
+ import { toREM } from "../utils/units.js";
10
+
9
11
  export const Skeleton = (props) => {
10
12
  const {
11
13
  variant = "text", // 'text', 'circular', 'avatar', 'image', 'card', 'listTile', 'button'
@@ -43,8 +45,12 @@ export const Skeleton = (props) => {
43
45
  return { width: "100%", height: 120, borderRadius: 12 };
44
46
  case "listTile":
45
47
  return { width: "100%", height: 56, borderRadius: 8 };
48
+ // The shape a button actually has: 40 units tall (the Fitts floor the Button
49
+ // preset uses) with the Button's own 16-unit corner, not an invented 36x24
50
+ // pill — a skeleton that does not match the control it replaces reads as a
51
+ // layout shift when the real one loads.
46
52
  case "button":
47
- return { width: 120, height: 36, borderRadius: 24 };
53
+ return { width: 120, height: 40, borderRadius: 16 };
48
54
  default:
49
55
  return { width: "100%", height: 20, borderRadius: 4 };
50
56
  }
@@ -79,14 +85,11 @@ export const Skeleton = (props) => {
79
85
  if (variant === "button") {
80
86
  return WidgetFactory({
81
87
  tag: "div",
82
- width: typeof finalWidth === "number" ? `${finalWidth}px` : finalWidth,
88
+ width: toREM(finalWidth),
83
89
  height:
84
- typeof finalHeight === "number" ? `${finalHeight}px` : finalHeight,
90
+ toREM(finalHeight),
85
91
  backgroundColor: bgColor,
86
- borderRadius:
87
- typeof finalBorderRadius === "number"
88
- ? `${finalBorderRadius}px`
89
- : finalBorderRadius,
92
+ borderRadius: toREM(finalBorderRadius),
90
93
  ...getAnimationStyle(),
91
94
  });
92
95
  }
@@ -97,14 +100,14 @@ export const Skeleton = (props) => {
97
100
  tag: "div",
98
101
  display: "flex",
99
102
  alignItems: "center",
100
- gap: "12px",
103
+ gap: 12,
101
104
  width: "100%",
102
105
  });
103
106
 
104
107
  const avatar = WidgetFactory({
105
108
  tag: "div",
106
- width: "40px",
107
- height: "40px",
109
+ width: 40,
110
+ height: 40,
108
111
  backgroundColor: bgColor,
109
112
  borderRadius: "50%",
110
113
  flexShrink: 0,
@@ -117,24 +120,24 @@ export const Skeleton = (props) => {
117
120
  flex: 1,
118
121
  display: "flex",
119
122
  flexDirection: "column",
120
- gap: "8px",
123
+ gap: 8,
121
124
  });
122
125
 
123
126
  const line1 = WidgetFactory({
124
127
  tag: "div",
125
128
  width: "80%",
126
- height: "14px",
129
+ height: 14,
127
130
  backgroundColor: bgColor,
128
- borderRadius: "4px",
131
+ borderRadius: 4,
129
132
  ...getAnimationStyle(),
130
133
  });
131
134
 
132
135
  const line2 = WidgetFactory({
133
136
  tag: "div",
134
137
  width: "60%",
135
- height: "12px",
138
+ height: 12,
136
139
  backgroundColor: bgColor,
137
- borderRadius: "4px",
140
+ borderRadius: 4,
138
141
  ...getAnimationStyle(),
139
142
  });
140
143
 
@@ -151,16 +154,16 @@ export const Skeleton = (props) => {
151
154
  tag: "div",
152
155
  display: "flex",
153
156
  flexDirection: "column",
154
- gap: "12px",
157
+ gap: 12,
155
158
  width: "100%",
156
159
  });
157
160
 
158
161
  const imageArea = WidgetFactory({
159
162
  tag: "div",
160
163
  width: "100%",
161
- height: "120px",
164
+ height: 120,
162
165
  backgroundColor: bgColor,
163
- borderRadius: "8px",
166
+ borderRadius: 8,
164
167
  ...getAnimationStyle(),
165
168
  });
166
169
  container.appendChild(imageArea);
@@ -169,25 +172,25 @@ export const Skeleton = (props) => {
169
172
  tag: "div",
170
173
  display: "flex",
171
174
  flexDirection: "column",
172
- gap: "8px",
173
- padding: "0 8px",
175
+ gap: 8,
176
+ paddingHorizontal: 8,
174
177
  });
175
178
 
176
179
  const line1 = WidgetFactory({
177
180
  tag: "div",
178
181
  width: "90%",
179
- height: "16px",
182
+ height: 16,
180
183
  backgroundColor: bgColor,
181
- borderRadius: "4px",
184
+ borderRadius: 4,
182
185
  ...getAnimationStyle(),
183
186
  });
184
187
 
185
188
  const line2 = WidgetFactory({
186
189
  tag: "div",
187
190
  width: "70%",
188
- height: "12px",
191
+ height: 12,
189
192
  backgroundColor: bgColor,
190
- borderRadius: "4px",
193
+ borderRadius: 4,
191
194
  ...getAnimationStyle(),
192
195
  });
193
196
 
@@ -201,14 +204,11 @@ export const Skeleton = (props) => {
201
204
  // Simple variants (text, circular, avatar, image)
202
205
  return WidgetFactory({
203
206
  tag: "div",
204
- width: typeof finalWidth === "number" ? `${finalWidth}px` : finalWidth,
207
+ width: toREM(finalWidth),
205
208
  height:
206
- typeof finalHeight === "number" ? `${finalHeight}px` : finalHeight,
209
+ toREM(finalHeight),
207
210
  backgroundColor: bgColor,
208
- borderRadius:
209
- typeof finalBorderRadius === "number"
210
- ? `${finalBorderRadius}px`
211
- : finalBorderRadius,
211
+ borderRadius: toREM(finalBorderRadius),
212
212
  ...getAnimationStyle(),
213
213
  });
214
214
  };
@@ -219,7 +219,7 @@ export const Skeleton = (props) => {
219
219
  widgetName: "Skeleton",
220
220
  display: "flex",
221
221
  flexDirection: "column",
222
- gap: typeof gap === "number" ? `${gap}px` : gap,
222
+ gap: toREM(gap),
223
223
  width: "100%",
224
224
  });
225
225
 
@@ -12,6 +12,9 @@ import { injectKeyframes } from "../utils/styleInjector.js";
12
12
  * Props owned by the Slider's closure state. Routed to the widget's `update`
13
13
  * and never to the factory's (see `composeUpdate`).
14
14
  */
15
+ import { toREM } from "../utils/units.js";
16
+ import { TAP_MINIMUM } from "../tools/index.js";
17
+
15
18
  const SLIDER_PROP_KEYS = [
16
19
  "value", "min", "max", "step", "disabled",
17
20
  "width", "height", "thumbSize",
@@ -32,7 +35,7 @@ export const Slider = (props) => {
32
35
  // Appearance
33
36
  width = "100%",
34
37
  height = 4,
35
- thumbSize = 20,
38
+ thumbSize = 24,
36
39
  color = colors.primary,
37
40
  trackColor = colors.border,
38
41
  thumbColor = "#fff",
@@ -45,7 +48,9 @@ export const Slider = (props) => {
45
48
  showMarks = false,
46
49
  marks = [], // ← implemented
47
50
  // Visual effects
48
- striped = true,
51
+ // Off by default: diagonal stripes turn a value control into a progress bar
52
+ // that is saying something, and a text-size slider is not saying anything.
53
+ striped = false,
49
54
  animatedStripes = false,
50
55
  stripeColor = "rgba(255,255,255,0.25)",
51
56
  glow = false,
@@ -72,10 +77,10 @@ export const Slider = (props) => {
72
77
  let displayPercent = inverted ? 1 - rawPercent : rawPercent;
73
78
 
74
79
  let isHorizontal = orientation === "horizontal";
75
- let trackSizePx = isHorizontal ? height : width;
76
- let trackLengthPx =
77
- typeof trackSizePx === "number" ? `${trackSizePx}px` : trackSizePx;
78
- let thumbSizeNum = typeof thumbSize === "number" ? thumbSize : 20;
80
+ let trackSize = isHorizontal ? height : width;
81
+ let trackLength =
82
+ toREM(trackSize);
83
+ let thumbSizeNum = typeof thumbSize === "number" ? thumbSize : 24;
79
84
 
80
85
  // Positioning retries while the track still measures 0 (hidden parent, not laid
81
86
  // out yet). Both handles are tracked so unmounting can stop them: an untracked
@@ -109,11 +114,11 @@ export const Slider = (props) => {
109
114
  valueDisplay = WidgetFactory({
110
115
  tag: "div",
111
116
  textContent: `${valuePrefix}${currentValue}${valueSuffix}`,
112
- fontSize: "14px",
117
+ fontSize: 14,
113
118
  color: colors.textSecondary,
114
119
  textAlign: "center",
115
- marginBottom: isHorizontal ? "4px" : "0",
116
- marginRight: !isHorizontal ? "4px" : "0",
120
+ marginBottom: isHorizontal ? 4 : 0,
121
+ marginRight: !isHorizontal ? 4 : 0,
117
122
  });
118
123
  container.appendChild(valueDisplay);
119
124
  }
@@ -125,12 +130,15 @@ export const Slider = (props) => {
125
130
  ...(isHorizontal
126
131
  ? {
127
132
  width: "100%",
128
- height: trackLengthPx,
133
+ height: trackLength,
134
+ // The rail is 4 units of paint; the surface a thumb can grab is 40.
135
+ minHeight: TAP_MINIMUM,
129
136
  display: "flex",
130
137
  alignItems: "center",
131
138
  }
132
139
  : {
133
140
  height: "100%", // for vertical, the wrapper takes the full height
141
+ minWidth: TAP_MINIMUM,
134
142
  display: "flex",
135
143
  justifyContent: "center",
136
144
  }),
@@ -144,14 +152,14 @@ export const Slider = (props) => {
144
152
  ...(isHorizontal
145
153
  ? {
146
154
  width: "100%",
147
- height: trackLengthPx,
155
+ height: trackLength,
148
156
  }
149
157
  : {
150
- width: trackLengthPx,
158
+ width: trackLength,
151
159
  height: "100%",
152
160
  }),
153
161
  backgroundColor: trackColor,
154
- borderRadius: trackLengthPx,
162
+ borderRadius: trackLength,
155
163
  cursor: disabled ? "not-allowed" : "pointer",
156
164
  });
157
165
  wrapper.appendChild(track);
@@ -161,7 +169,7 @@ export const Slider = (props) => {
161
169
  tag: "div",
162
170
  position: "absolute",
163
171
  backgroundColor: color,
164
- borderRadius: trackLengthPx,
172
+ borderRadius: trackLength,
165
173
  transition: disabled ? "none" : "width 0.1s ease, height 0.1s ease",
166
174
  ...(isHorizontal
167
175
  ? {
@@ -191,8 +199,8 @@ export const Slider = (props) => {
191
199
  const thumb = WidgetFactory({
192
200
  tag: "div",
193
201
  position: "absolute",
194
- width: `${thumbSizeNum}px`,
195
- height: `${thumbSizeNum}px`,
202
+ width: toREM(thumbSizeNum),
203
+ height: toREM(thumbSizeNum),
196
204
  backgroundColor: thumbColor,
197
205
  borderRadius: "50%",
198
206
  boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
@@ -350,12 +358,15 @@ export const Slider = (props) => {
350
358
  }
351
359
  if (isHorizontal) {
352
360
  const left = displayPercent * trackRect.width;
353
- thumb.style.left = `${left - thumbSizeNum / 2}px`;
361
+ // The half-thumb comes from the rendered box, not from `thumbSize`: that
362
+ // number is a framework unit and the handle is `rem`, so at a raised font
363
+ // size the handle is wider than the arithmetic says and sits off-centre.
364
+ thumb.style.left = `${left - thumb.offsetWidth / 2}px`;
354
365
  thumb.style.top = "50%";
355
366
  thumb.style.transform = "translateY(-50%)";
356
367
  } else {
357
368
  const bottom = displayPercent * trackRect.height;
358
- thumb.style.bottom = `${bottom - thumbSizeNum / 2}px`;
369
+ thumb.style.bottom = `${bottom - thumb.offsetHeight / 2}px`;
359
370
  thumb.style.left = "50%";
360
371
  thumb.style.transform = "translateX(-50%)";
361
372
  }
@@ -5,27 +5,9 @@
5
5
  import { WidgetFactory } from "../widget-factory/index.js";
6
6
  import { border } from "../tools/index.js";
7
7
  import { colors } from "../utils/themes.js";
8
+ import { readableInk } from "../utils/readableInk.js";
8
9
 
9
- // Fallback colors (if the theme is not initialized yet)
10
- const FALLBACK = {
11
- gray800: "#1e293b",
12
- primary: "#6366f1",
13
- success: "#10b981",
14
- danger: "#ef4444",
15
- warning: "#f59e0b",
16
- info: "#3b82f6",
17
- gray900: "#0f172a",
18
- white: "#ffffff",
19
- };
20
-
21
- // Safe function to get theme color
22
- const getThemeColor = (name, fallback) => {
23
- try {
24
- return (colors && colors[name]) || fallback;
25
- } catch {
26
- return fallback;
27
- }
28
- };
10
+ import { toREM } from "../utils/units.js";
29
11
 
30
12
  /**
31
13
  * SnackBar - Notificación temporal
@@ -54,7 +36,20 @@ export function SnackBar(options) {
54
36
  onClose,
55
37
  } = options;
56
38
 
57
- // Presets using theme colors (with fallback)
39
+ // A preset plate is a solid signal colour, and the theme's ink is not always
40
+ // readable on it: in dark mode `colors.text` is near-white, which measured
41
+ // 1.9:1 on the green. The label follows the plate it is painted on.
42
+ const plateInk = (bg) => {
43
+ if (bg === colors.primary) return colors.onPrimary;
44
+ if (bg === colors.secondary) return colors.onSecondary;
45
+ return readableInk(bg, colors.text);
46
+ };
47
+
48
+ const successInk = plateInk(colors.success);
49
+ const dangerInk = plateInk(colors.danger);
50
+ const warningInk = plateInk(colors.warning);
51
+ const infoInk = plateInk(colors.info);
52
+
58
53
  const presets = {
59
54
  normal: {
60
55
  bg: colors.surface,
@@ -63,42 +58,43 @@ export function SnackBar(options) {
63
58
  },
64
59
  success: {
65
60
  bg: colors.success,
66
- text: colors.text,
67
- action: colors.primary,
61
+ text: successInk,
62
+ action: successInk,
68
63
  },
69
64
  error: {
70
65
  bg: colors.danger,
71
- text: colors.text,
72
- action: colors.primary,
66
+ text: dangerInk,
67
+ action: dangerInk,
73
68
  },
74
69
  warning: {
75
70
  bg: colors.warning,
76
- text: colors.text,
77
- action: getThemeColor("gray900", FALLBACK.gray900),
71
+ text: warningInk,
72
+ action: warningInk,
78
73
  },
79
74
  info: {
80
75
  bg: colors.info,
81
- text: colors.text,
82
- action: FALLBACK.white,
76
+ text: infoInk,
77
+ action: infoInk,
83
78
  },
84
79
  };
85
80
 
86
81
  const preset = presets[type] || presets.normal;
87
82
  const finalBg = customBg || preset.bg;
88
- const finalText = customText || preset.text;
89
- const finalAction = customAction || preset.action;
83
+ const finalText = customText || (customBg ? plateInk(finalBg) : preset.text);
84
+ const finalAction =
85
+ customAction || (customBg ? finalText : preset.action);
90
86
 
91
87
  // Createste main container
92
88
  const snackbar = WidgetFactory({
93
89
  tag: "div",
94
90
  position: "fixed",
95
- left: `${margin}px`,
91
+ left: toREM(margin),
96
92
  // borderRadius: borderRadius,
97
- right: `${margin}px`,
98
- [position]: `${margin}px`,
93
+ right: toREM(margin),
94
+ [position]: toREM(margin),
99
95
  backgroundColor: finalBg,
100
96
  borderRadius:
101
- typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius,
97
+ toREM(borderRadius),
102
98
  padding: padding,
103
99
  border: border(1, "solid", colors.gray100),
104
100
  zIndex: zIndex,
@@ -109,8 +105,8 @@ export function SnackBar(options) {
109
105
  opacity: 0,
110
106
  transform:
111
107
  position === "bottom"
112
- ? `translateY(calc(100% + ${margin}px))`
113
- : `translateY(calc(-100% - ${margin}px))`,
108
+ ? `translateY(calc(100% + ${toREM(margin)}))`
109
+ : `translateY(calc(-100% - ${toREM(margin)}))`,
114
110
  transition: `opacity ${animationDuration}ms ease, transform ${animationDuration}ms ease`,
115
111
  display: "flex",
116
112
  alignItems: "center",
@@ -156,8 +152,8 @@ export function SnackBar(options) {
156
152
  snackbar.style.opacity = "0";
157
153
  snackbar.style.transform =
158
154
  position === "bottom"
159
- ? `translateY(calc(100% + ${margin}px))`
160
- : `translateY(calc(-100% - ${margin}px))`;
155
+ ? `translateY(calc(100% + ${toREM(margin)}))`
156
+ : `translateY(calc(-100% - ${toREM(margin)}))`;
161
157
  setTimeout(() => {
162
158
  if (snackbar.parentNode) snackbar.parentNode.removeChild(snackbar);
163
159
  if (onClose) onClose();
@@ -11,6 +11,8 @@ import { Icon } from "./Icon.js";
11
11
  import { Button } from "./Button.js";
12
12
  import { colors } from "../utils/themes.js";
13
13
 
14
+ import { toREM } from "../utils/units.js";
15
+
14
16
  export const Stepper = (props) => {
15
17
  const {
16
18
  steps = [],
@@ -32,7 +34,7 @@ export const Stepper = (props) => {
32
34
  borderColor = colors.border,
33
35
  borderWidth = 1,
34
36
  shadow,
35
- padding = 20,
37
+ padding = 24,
36
38
  margin = 0,
37
39
  width = "100%",
38
40
  ...rest
@@ -85,7 +87,7 @@ export const Stepper = (props) => {
85
87
  bgColor: isActive
86
88
  ? colors.primary
87
89
  : isCompleted
88
- ? colors.success
90
+ ? colors.successPlate
89
91
  : colors.gray300,
90
92
  justifyContent: "center",
91
93
  alignItems: "center",
@@ -95,7 +97,9 @@ export const Stepper = (props) => {
95
97
  text: String(idx + 1),
96
98
  size: 14,
97
99
  weight: "bold",
98
- color: isActive ? "#fff" : colors.textSecondary,
100
+ // On the grey step plate, `textSecondary` measured 4.07:1 in
101
+ // dark; gray700 stays muted and clears AA on both themes.
102
+ color: isActive ? colors.onPrimary : colors.gray700,
99
103
  }),
100
104
  });
101
105
  } else if (variant === "numbers") {
@@ -103,13 +107,13 @@ export const Stepper = (props) => {
103
107
  text: String(idx + 1),
104
108
  size: 16,
105
109
  weight: "bold",
106
- color: isActive ? colors.primary : colors.textSecondary,
110
+ color: isActive ? colors.primary : colors.gray700,
107
111
  });
108
112
  } else {
109
113
  indicator = Icon({
110
114
  name: step.icon || "circle",
111
115
  size: 24,
112
- color: isActive ? colors.primary : colors.gray300,
116
+ color: isActive ? colors.primary : colors.gray500,
113
117
  });
114
118
  }
115
119
 
@@ -215,12 +219,12 @@ export const Stepper = (props) => {
215
219
 
216
220
  // Build style for main container using WidgetFactory
217
221
  let style = {
218
- width: typeof width === "number" ? `${width}px` : width,
222
+ width: toREM(width),
219
223
  backgroundColor: bgColor,
220
224
  borderRadius:
221
- typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius,
222
- padding: typeof padding === "number" ? `${padding}px` : padding,
223
- margin: typeof margin === "number" ? `${margin}px` : margin,
225
+ toREM(borderRadius),
226
+ padding: toREM(padding),
227
+ margin: toREM(margin),
224
228
  display: "flex",
225
229
  flexDirection: "column",
226
230
  gap: "24px",
@@ -3,7 +3,10 @@
3
3
  * @description iOS-style toggle switch 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 { toREM } from "../utils/units.js";
9
+ import { PILL, TAP_MINIMUM, LABEL_TEXT } from "../tools/index.js";
7
10
 
8
11
  export const Switch = (props) => {
9
12
  const {
@@ -13,6 +16,7 @@ export const Switch = (props) => {
13
16
  disabled = false,
14
17
  size = "medium",
15
18
  activeColor = colors.success,
19
+ label = null,
16
20
  ...rest
17
21
  } = props;
18
22
 
@@ -20,13 +24,19 @@ export const Switch = (props) => {
20
24
  // onToggle kept as alias for backward compatibility
21
25
  const onPress = onPressProp || onToggle;
22
26
 
23
- // Sizes
27
+ // Sizes. Track height is knob + 2 x pad, so the knob can never be asked to sit
28
+ // outside the pill it lives in.
24
29
  const sizes = {
25
- small: { width: 40, height: 24, knob: 20 },
26
- medium: { width: 51, height: 31, knob: 27 },
27
- large: { width: 60, height: 36, knob: 32 },
30
+ small: { width: 40, height: 24, knob: 16 },
31
+ medium: { width: 48, height: 32, knob: 24 },
32
+ large: { width: 64, height: 40, knob: 32 },
28
33
  };
29
34
  const sz = sizes[size] || sizes.medium;
35
+ const PAD = 4;
36
+ /** The knob's travel, in framework units. It has to be rem, like the track: a
37
+ * `px` offset here slides the knob short of the edge the moment the reader
38
+ * raises their font and the (rem) track grows around it. */
39
+ const travel = () => toREM(sz.width - sz.knob - PAD * 2);
30
40
 
31
41
  let internalValue = value;
32
42
  let switchRef = null;
@@ -37,9 +47,7 @@ export const Switch = (props) => {
37
47
  switchRef.style.backgroundColor = newValue ? activeColor : colors.gray300;
38
48
  }
39
49
  if (knobRef) {
40
- knobRef.style.transform = newValue
41
- ? `translateX(${sz.width - sz.knob - 4}px)`
42
- : "translateX(0)";
50
+ knobRef.style.transform = newValue ? `translateX(${travel()})` : "translateX(0)";
43
51
  }
44
52
  };
45
53
 
@@ -49,32 +57,65 @@ export const Switch = (props) => {
49
57
  height: sz.knob,
50
58
  backgroundColor: "#ffffff",
51
59
  borderRadius: "50%",
52
- transform: internalValue
53
- ? `translateX(${sz.width - sz.knob - 4}px)`
54
- : "translateX(0)",
60
+ transform: internalValue ? `translateX(${travel()})` : "translateX(0)",
55
61
  transition: "transform 0.2s ease",
56
62
  boxShadow: "0 1px 3px rgba(0,0,0,0.2)",
57
63
  });
58
64
  knobRef = knob;
59
65
 
60
- // Createste main switch container
61
- const switchContainer = WidgetFactory({
62
- widgetName: "Switch",
66
+ // The visible pill. Everything the reader sees is painted here.
67
+ const track = WidgetFactory({
63
68
  width: sz.width,
64
69
  height: sz.height,
65
70
  backgroundColor: internalValue ? activeColor : colors.gray300,
66
- borderRadius: sz.height / 2,
71
+ borderRadius: PILL,
67
72
  display: "flex",
68
73
  alignItems: "center",
69
- padding: "2px",
74
+ padding: PAD,
70
75
  transition: "all 0.2s ease",
71
- cursor: disabled ? "not-allowed" : "pointer",
72
- opacity: disabled ? 0.5 : 1,
73
76
  boxSizing: "border-box",
74
77
  child: knob,
78
+ });
79
+ switchRef = track;
80
+
81
+ // The pressable box is the track's parent, not the track: a 32-unit pill is not
82
+ // a Fitts target, and stretching the pill to 40 would make it look like a
83
+ // button. The hit area grows, the switch does not.
84
+ //
85
+ // With a `label` the box grows further into the whole row, because a reader
86
+ // aiming at the words must still land on the toggle: the label is part of the
87
+ // control, not a caption beside it.
88
+ const isLabelled = label !== null && label !== undefined && label !== "";
89
+
90
+ const switchContainer = WidgetFactory({
91
+ widgetName: "Switch",
92
+ display: "flex",
93
+ flexDirection: "row",
94
+ alignItems: "center",
95
+ justifyContent: isLabelled ? "space-between" : "center",
96
+ gap: 12,
97
+ width: isLabelled ? "100%" : undefined,
98
+ minHeight: TAP_MINIMUM,
99
+ cursor: disabled ? "not-allowed" : "pointer",
100
+ opacity: disabled ? 0.5 : 1,
101
+ // The root carries the click, so the default press effect scaled whatever
102
+ // the root happens to be — with a label that is the whole row, and the words
103
+ // shrank with the switch. The knob's slide is this control's feedback; the
104
+ // row must not behave like a button.
105
+ disableTransform: true,
106
+ child: isLabelled
107
+ ? [
108
+ Text({
109
+ text: label,
110
+ size: LABEL_TEXT,
111
+ fontWeight: 500,
112
+ color: disabled ? colors.textDisabled : colors.text,
113
+ }),
114
+ track,
115
+ ]
116
+ : track,
75
117
  ...rest,
76
118
  });
77
- switchRef = switchContainer;
78
119
 
79
120
  // Handle click
80
121
  if (!disabled) {