flet-box 0.0.0-stage → 1.0.2
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.
- package/LICENSE +21 -0
- package/README.md +190 -2
- package/bin/cli.js +394 -0
- package/bin/commands/buildAndroid.js +200 -0
- package/bin/commands/buildIOS.js +237 -0
- package/bin/commands/create.js +406 -0
- package/bin/commands/createBundle.js +375 -0
- package/bin/commands/createComponent.js +115 -0
- package/bin/commands/createScreen.js +161 -0
- package/bin/commands/killServer.js +107 -0
- package/bin/commands/package.js +131 -0
- package/bin/commands/release.js +189 -0
- package/bin/commands/runBundle.js +62 -0
- package/bin/commands/runCloudflare.js +89 -0
- package/bin/commands/runNative.js +109 -0
- package/bin/commands/runServer.js +390 -0
- package/bin/ota/loader.js +361 -0
- package/bin/utils/bundleConfig.js +178 -0
- package/bin/utils/capacitor.js +137 -0
- package/bin/utils/colors.js +215 -0
- package/bin/utils/helpers.js +80 -0
- package/bin/utils/spinner.js +96 -0
- package/bin/utils/templates.js +1824 -0
- package/bin/utils/version.js +108 -0
- package/package.json +58 -4
- package/src/animations/AnimatedBox.js +607 -0
- package/src/animations/AnimatedText.js +105 -0
- package/src/animations/MatrixRain.js +220 -0
- package/src/animations/ParallaxBox.js +369 -0
- package/src/animations/animate.js +332 -0
- package/src/animations/index.js +25 -0
- package/src/app.js +41 -0
- package/src/assets/icon-1024-maskable.png +0 -0
- package/src/assets/icon-192-maskable.png +0 -0
- package/src/assets/icon-192.png +0 -0
- package/src/assets/icon-512-maskable.png +0 -0
- package/src/assets/icon-512.png +0 -0
- package/src/assets/logo.png +0 -0
- package/src/core/App.js +44 -0
- package/src/core/hmr-client.js +142 -0
- package/src/core/index.js +34 -0
- package/src/core/pwa.js +100 -0
- package/src/core/runApp.js +430 -0
- package/src/create-index.sh +237 -0
- package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
- package/src/fonts/icons.css +23 -0
- package/src/index.d.ts +1930 -0
- package/src/index.js +288 -0
- package/src/navigations/AdaptiveScaffold.js +149 -0
- package/src/navigations/AppBar.js +331 -0
- package/src/navigations/BottomNavigation.js +298 -0
- package/src/navigations/CollapsibleSideBar.js +139 -0
- package/src/navigations/Drawer.js +333 -0
- package/src/navigations/DrawerItem.js +260 -0
- package/src/navigations/Router.js +475 -0
- package/src/navigations/Scaffold.js +537 -0
- package/src/navigations/Tabs.js +384 -0
- package/src/navigations/index.js +57 -0
- package/src/services/RamStore.js +213 -0
- package/src/services/Session.js +310 -0
- package/src/services/Storage.js +309 -0
- package/src/services/http.js +350 -0
- package/src/services/index.js +57 -0
- package/src/tools/animation.js +42 -0
- package/src/tools/array.js +95 -0
- package/src/tools/border.js +28 -0
- package/src/tools/clipboard.js +104 -0
- package/src/tools/color.js +329 -0
- package/src/tools/createList.js +215 -0
- package/src/tools/delay.js +55 -0
- package/src/tools/device.js +138 -0
- package/src/tools/dict.js +297 -0
- package/src/tools/dimensions.js +118 -0
- package/src/tools/filter.js +41 -0
- package/src/tools/flex.js +26 -0
- package/src/tools/gradient.js +34 -0
- package/src/tools/grid.js +57 -0
- package/src/tools/index.js +38 -0
- package/src/tools/mapList.js +107 -0
- package/src/tools/margin.js +93 -0
- package/src/tools/memo.js +60 -0
- package/src/tools/os.js +104 -0
- package/src/tools/padding.js +93 -0
- package/src/tools/print.js +69 -0
- package/src/tools/random.js +299 -0
- package/src/tools/ref.js +70 -0
- package/src/tools/rgba.js +4 -0
- package/src/tools/shadow.js +23 -0
- package/src/tools/string.js +85 -0
- package/src/tools/time.js +674 -0
- package/src/tools/transform.js +68 -0
- package/src/tools/transition.js +48 -0
- package/src/tools/useState.js +268 -0
- package/src/tools/uuid.js +42 -0
- package/src/utils/TextInputValidator.js +256 -0
- package/src/utils/composeUpdate.js +65 -0
- package/src/utils/getWidgetProps.js +35 -0
- package/src/utils/index.js +76 -0
- package/src/utils/markdownParser.js +368 -0
- package/src/utils/mediaTime.js +79 -0
- package/src/utils/navigation.js +263 -0
- package/src/utils/scriptInjector.js +73 -0
- package/src/utils/stackPosition.js +31 -0
- package/src/utils/stopWebRefresh.js +77 -0
- package/src/utils/styleInjector.js +38 -0
- package/src/utils/syntaxHighlight.js +339 -0
- package/src/utils/themes.js +347 -0
- package/src/utils/units.js +87 -0
- package/src/utils/visualEffects.js +169 -0
- package/src/widget-factory/addChildren.js +114 -0
- package/src/widget-factory/assignProps.js +256 -0
- package/src/widget-factory/createWidget.js +28 -0
- package/src/widget-factory/effects.js +132 -0
- package/src/widget-factory/index.js +7 -0
- package/src/widget-factory/lifecycle.js +210 -0
- package/src/widget-factory/makeParentable.js +89 -0
- package/src/widget-factory/pixTool.js +40 -0
- package/src/widget-factory/processProps.js +122 -0
- package/src/widget-factory/reactivity.js +98 -0
- package/src/widget-factory/remTool.js +34 -0
- package/src/widget-factory/stackPosition.js +87 -0
- package/src/widget-factory/stringTool.js +26 -0
- package/src/widget-factory/tools.js +249 -0
- package/src/widget-factory/translateProps.js +1049 -0
- package/src/widget-factory/warnings.js +284 -0
- package/src/widget-factory/widgetFactory.js +200 -0
- package/src/widgets/Accordion.js +562 -0
- package/src/widgets/AdMob.js +315 -0
- package/src/widgets/AdSense.js +268 -0
- package/src/widgets/AlertDialog.js +162 -0
- package/src/widgets/Audio.js +311 -0
- package/src/widgets/Avatar.js +210 -0
- package/src/widgets/Badge.js +189 -0
- package/src/widgets/BottomSheet.js +287 -0
- package/src/widgets/Button.js +247 -0
- package/src/widgets/Card.js +33 -0
- package/src/widgets/Carousel.js +301 -0
- package/src/widgets/Chart.js +632 -0
- package/src/widgets/Checkbox.js +106 -0
- package/src/widgets/Chip.js +153 -0
- package/src/widgets/CircularBar.js +352 -0
- package/src/widgets/CircularChart.js +549 -0
- package/src/widgets/CodeViewer.js +236 -0
- package/src/widgets/Column.js +29 -0
- package/src/widgets/Container.js +31 -0
- package/src/widgets/DataTable.js +257 -0
- package/src/widgets/Divider.js +93 -0
- package/src/widgets/DraggBox.js +115 -0
- package/src/widgets/Dropdown.js +505 -0
- package/src/widgets/DroppBox.js +180 -0
- package/src/widgets/FloatingActionButton.js +91 -0
- package/src/widgets/GridView.js +20 -0
- package/src/widgets/Icon.js +33 -0
- package/src/widgets/Image.js +15 -0
- package/src/widgets/Input.js +565 -0
- package/src/widgets/Inspector.js +254 -0
- package/src/widgets/InstallButton.js +110 -0
- package/src/widgets/ListTile.js +224 -0
- package/src/widgets/ListView.js +503 -0
- package/src/widgets/Markdown.js +1236 -0
- package/src/widgets/Modal.js +496 -0
- package/src/widgets/Pagination.js +195 -0
- package/src/widgets/ProgressBar.js +178 -0
- package/src/widgets/QRCode.js +467 -0
- package/src/widgets/Radio.js +132 -0
- package/src/widgets/Rating.js +261 -0
- package/src/widgets/Row.js +29 -0
- package/src/widgets/Skeleton.js +243 -0
- package/src/widgets/Slider.js +527 -0
- package/src/widgets/SnackBar.js +227 -0
- package/src/widgets/Stack.js +29 -0
- package/src/widgets/Stepper.js +296 -0
- package/src/widgets/Switch.js +102 -0
- package/src/widgets/Text.js +98 -0
- package/src/widgets/Tooltip.js +267 -0
- package/src/widgets/TreeView.js +370 -0
- package/src/widgets/Video.js +92 -0
- package/src/widgets/index.js +52 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// navigations/AppBar.js
|
|
2
|
+
import { WidgetFactory } from "../widget-factory/index.js";
|
|
3
|
+
import { Container } from "../widgets/Container.js";
|
|
4
|
+
import { Row } from "../widgets/Row.js";
|
|
5
|
+
import { Text } from "../widgets/Text.js";
|
|
6
|
+
import { Icon } from "../widgets/Icon.js";
|
|
7
|
+
import { colors } from "../utils/themes.js";
|
|
8
|
+
import { goBack, goTo } from "./Router.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} AppBarProps
|
|
12
|
+
* @property {string|HTMLElement} [title] - Page title shown in the center or left of the bar.
|
|
13
|
+
* @property {HTMLElement|null} [leading=null] - Widget placed at the leading (left) edge, e.g. a menu or nav icon.
|
|
14
|
+
* @property {HTMLElement[]} [actions=[]] - Action widgets placed at the trailing (right) edge.
|
|
15
|
+
* @property {number} [actionsGap=12] - Gap in pixels between action widgets.
|
|
16
|
+
* @property {string} [backgroundColor] - Background fill color.
|
|
17
|
+
* @property {string|null} [gradient=null] - CSS gradient string; overrides backgroundColor when set.
|
|
18
|
+
* @property {string} [titleColor] - Color of the title text.
|
|
19
|
+
* @property {string} [iconColor] - Color of icons; defaults to titleColor.
|
|
20
|
+
* @property {number} [elevation=2] - Shadow depth (0–5).
|
|
21
|
+
* @property {boolean} [centerTitle=false] - Whether to center the title horizontally.
|
|
22
|
+
* @property {number} [titleSize=20] - Font size of the title in pixels.
|
|
23
|
+
* @property {string} [titleWeight="500"] - Font weight of the title.
|
|
24
|
+
* @property {boolean} [showBackButton=false] - Auto-inject a back-navigation arrow as the leading widget.
|
|
25
|
+
* @property {string|null} [backButtonRoute=null] - Route to push when the back button is pressed; falls back to goBack().
|
|
26
|
+
* @property {Function|null} [onBackPress=null] - Custom handler for back-button press; takes priority over backButtonRoute.
|
|
27
|
+
* @property {number} [margin=0] - Uniform outer margin.
|
|
28
|
+
* @property {number} [marginTop=0] - Top outer margin (overrides margin).
|
|
29
|
+
* @property {number} [marginBottom=0] - Bottom outer margin (overrides margin).
|
|
30
|
+
* @property {number} [marginLeft=0] - Left outer margin (overrides margin).
|
|
31
|
+
* @property {number} [marginRight=0] - Right outer margin (overrides margin).
|
|
32
|
+
* @property {number} [padding=0] - Uniform inner padding.
|
|
33
|
+
* @property {number} [paddingTop=0] - Top inner padding (overrides padding).
|
|
34
|
+
* @property {number} [paddingBottom=0] - Bottom inner padding (overrides padding).
|
|
35
|
+
* @property {number} [paddingLeft=4] - Left inner padding (overrides padding).
|
|
36
|
+
* @property {number} [paddingRight=24] - Right inner padding (overrides padding).
|
|
37
|
+
* @property {number|string} [borderRadius=0] - Border radius (number = pixels, string = CSS value).
|
|
38
|
+
* @property {boolean|string} [shadow=true] - Whether to apply a shadow; pass a CSS string to use a custom shadow.
|
|
39
|
+
* @property {boolean} [sticky=true] - Whether the bar sticks to the top of the viewport on scroll.
|
|
40
|
+
* @property {boolean} [hideOnScroll=false] - Whether to animate the bar out of view when scrolling down.
|
|
41
|
+
* @property {number} [scrollThreshold=100] - Scroll distance in pixels before the hide animation triggers.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* AppBar renders a top navigation bar with a leading zone, a title, and
|
|
46
|
+
* an optional set of action widgets on the right.
|
|
47
|
+
*
|
|
48
|
+
* The bar can stick to the top of the viewport (`sticky`), hide when
|
|
49
|
+
* the user scrolls down (`hideOnScroll`), and auto-inject a back button
|
|
50
|
+
* (`showBackButton`) wired to the router or a custom handler.
|
|
51
|
+
*
|
|
52
|
+
* Non-HTMLElement entries in `actions` are silently filtered out to prevent
|
|
53
|
+
* "[object Object]" from being rendered.
|
|
54
|
+
*
|
|
55
|
+
* @param {AppBarProps} props
|
|
56
|
+
* @returns {HTMLElement} The `<header>` element representing the app bar,
|
|
57
|
+
* augmented with the following methods:
|
|
58
|
+
* - `setTitle(newTitle: string)` — update the title text in place.
|
|
59
|
+
* - `setBackgroundColor(color: string)` — swap the background color or gradient.
|
|
60
|
+
* - `show()` / `hide()` — toggle bar visibility.
|
|
61
|
+
*/
|
|
62
|
+
export const AppBar = (props) => {
|
|
63
|
+
const {
|
|
64
|
+
title,
|
|
65
|
+
leading = null,
|
|
66
|
+
actions = [],
|
|
67
|
+
actionsGap = 12,
|
|
68
|
+
|
|
69
|
+
backgroundColor = colors.surface,
|
|
70
|
+
gradient = null,
|
|
71
|
+
titleColor = colors.text,
|
|
72
|
+
iconColor = titleColor,
|
|
73
|
+
elevation = 2,
|
|
74
|
+
centerTitle = false,
|
|
75
|
+
titleSize = 20,
|
|
76
|
+
titleWeight = "500",
|
|
77
|
+
|
|
78
|
+
showBackButton = false,
|
|
79
|
+
backButtonRoute = null,
|
|
80
|
+
onBackPress = null,
|
|
81
|
+
|
|
82
|
+
margin = 0,
|
|
83
|
+
marginTop = 0,
|
|
84
|
+
marginBottom = 0,
|
|
85
|
+
marginLeft = 0,
|
|
86
|
+
marginRight = 0,
|
|
87
|
+
|
|
88
|
+
padding = 0,
|
|
89
|
+
paddingTop = 0,
|
|
90
|
+
paddingBottom = 0,
|
|
91
|
+
paddingLeft = 4,
|
|
92
|
+
paddingRight = 24,
|
|
93
|
+
borderRadius = 0,
|
|
94
|
+
shadow = true,
|
|
95
|
+
|
|
96
|
+
sticky = true,
|
|
97
|
+
hideOnScroll = false,
|
|
98
|
+
scrollThreshold = 100,
|
|
99
|
+
...rest
|
|
100
|
+
} = props;
|
|
101
|
+
|
|
102
|
+
// Guard: only HTMLElement instances are valid action widgets.
|
|
103
|
+
// Filtering here prevents the common mistake of passing component results
|
|
104
|
+
// that accidentally resolved to plain objects.
|
|
105
|
+
const validActions = Array.isArray(actions)
|
|
106
|
+
? actions.filter((action) => action instanceof HTMLElement)
|
|
107
|
+
: [];
|
|
108
|
+
|
|
109
|
+
// Resolve shorthand margin/padding props — individual sides win over the
|
|
110
|
+
// uniform shorthand when both are provided.
|
|
111
|
+
const finalMarginTop = marginTop || margin;
|
|
112
|
+
const finalMarginBottom = marginBottom || margin;
|
|
113
|
+
const finalMarginLeft = marginLeft || margin;
|
|
114
|
+
const finalMarginRight = marginRight || margin;
|
|
115
|
+
|
|
116
|
+
const finalPaddingTop = paddingTop || padding;
|
|
117
|
+
const finalPaddingBottom = paddingBottom || padding;
|
|
118
|
+
const finalPaddingLeft = paddingLeft || padding;
|
|
119
|
+
const finalPaddingRight = paddingRight || padding;
|
|
120
|
+
|
|
121
|
+
const finalBorderRadius =
|
|
122
|
+
typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius;
|
|
123
|
+
|
|
124
|
+
// Material Design elevation → box-shadow lookup table.
|
|
125
|
+
const shadows = {
|
|
126
|
+
0: "none",
|
|
127
|
+
1: "0 1px 3px rgba(0,0,0,0.12)",
|
|
128
|
+
2: "0 3px 6px rgba(0,0,0,0.16)",
|
|
129
|
+
3: "0 6px 12px rgba(0,0,0,0.2)",
|
|
130
|
+
4: "0 10px 20px rgba(0,0,0,0.25)",
|
|
131
|
+
5: "0 15px 30px rgba(0,0,0,0.3)",
|
|
132
|
+
};
|
|
133
|
+
const boxShadow = shadow === true ? shadows[elevation] : shadow || "none";
|
|
134
|
+
|
|
135
|
+
// ========== LEADING SECTION ==========
|
|
136
|
+
// Auto-create a back button if requested and no custom leading was supplied.
|
|
137
|
+
let finalLeading = leading;
|
|
138
|
+
if (showBackButton && !finalLeading) {
|
|
139
|
+
const backButton = Icon({
|
|
140
|
+
name: "arrow_back",
|
|
141
|
+
size: 24,
|
|
142
|
+
color: iconColor,
|
|
143
|
+
cursor: "pointer",
|
|
144
|
+
});
|
|
145
|
+
backButton.onclick = () => {
|
|
146
|
+
if (onBackPress) onBackPress();
|
|
147
|
+
else if (backButtonRoute) goTo(backButtonRoute);
|
|
148
|
+
else goBack();
|
|
149
|
+
};
|
|
150
|
+
finalLeading = backButton;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ========== BACKGROUND ==========
|
|
154
|
+
// Gradient takes priority over a solid background color.
|
|
155
|
+
let backgroundStyle = {};
|
|
156
|
+
if (gradient) {
|
|
157
|
+
backgroundStyle = { backgroundImage: gradient };
|
|
158
|
+
} else if (backgroundColor) {
|
|
159
|
+
backgroundStyle = { backgroundColor: backgroundColor };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ========== MAIN APP BAR ELEMENT ==========
|
|
163
|
+
const appBar = WidgetFactory({
|
|
164
|
+
tag: "header",
|
|
165
|
+
display: "flex",
|
|
166
|
+
alignItems: "center",
|
|
167
|
+
justifyContent: "space-between",
|
|
168
|
+
minHeight: 56,
|
|
169
|
+
borderRadius: finalBorderRadius,
|
|
170
|
+
marginTop: finalMarginTop,
|
|
171
|
+
marginBottom: finalMarginBottom,
|
|
172
|
+
marginLeft: finalMarginLeft,
|
|
173
|
+
marginRight: finalMarginRight,
|
|
174
|
+
paddingTop: `calc(${finalPaddingTop}px + env(safe-area-inset-top, 0px))`,
|
|
175
|
+
paddingBottom: finalPaddingBottom,
|
|
176
|
+
paddingLeft: finalPaddingLeft,
|
|
177
|
+
paddingRight: finalPaddingRight,
|
|
178
|
+
boxShadow: boxShadow,
|
|
179
|
+
flexShrink: 0,
|
|
180
|
+
position: sticky ? "sticky" : "relative",
|
|
181
|
+
top: sticky ? 0 : "auto",
|
|
182
|
+
zIndex: sticky ? 100 : "auto",
|
|
183
|
+
transition: "transform 0.3s ease, opacity 0.3s ease",
|
|
184
|
+
...backgroundStyle,
|
|
185
|
+
...rest,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ========== LEFT SECTION ==========
|
|
189
|
+
// Fixed minimum width ensures the center title stays visually centered
|
|
190
|
+
// even when no leading widget is present.
|
|
191
|
+
const leftSection = Container({
|
|
192
|
+
display: "flex",
|
|
193
|
+
alignItems: "center",
|
|
194
|
+
bgColor: "transparent",
|
|
195
|
+
justifyContent: "flex-start",
|
|
196
|
+
minWidth: 48,
|
|
197
|
+
flexShrink: 0,
|
|
198
|
+
child: finalLeading instanceof HTMLElement ? finalLeading : null,
|
|
199
|
+
});
|
|
200
|
+
appBar.appendChild(leftSection);
|
|
201
|
+
|
|
202
|
+
// ========== TITLE ==========
|
|
203
|
+
let titleElement;
|
|
204
|
+
if (typeof title === "string") {
|
|
205
|
+
titleElement = Text({
|
|
206
|
+
text: title,
|
|
207
|
+
size: titleSize,
|
|
208
|
+
weight: titleWeight,
|
|
209
|
+
color: titleColor,
|
|
210
|
+
whiteSpace: "nowrap",
|
|
211
|
+
overflow: "hidden",
|
|
212
|
+
textOverflow: "ellipsis",
|
|
213
|
+
letterSpacing: "0.15px",
|
|
214
|
+
});
|
|
215
|
+
} else if (title instanceof HTMLElement) {
|
|
216
|
+
titleElement = title;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const centerSection = Container({
|
|
220
|
+
flex: centerTitle ? 1 : "none",
|
|
221
|
+
display: "flex",
|
|
222
|
+
alignItems: "center",
|
|
223
|
+
justifyContent: centerTitle ? "center" : "flex-start",
|
|
224
|
+
padding: "0 8px",
|
|
225
|
+
bgColor: "transparent",
|
|
226
|
+
overflow: "hidden",
|
|
227
|
+
minWidth: 0,
|
|
228
|
+
child: titleElement,
|
|
229
|
+
});
|
|
230
|
+
appBar.appendChild(centerSection);
|
|
231
|
+
|
|
232
|
+
// ========== RIGHT SECTION (only rendered when there are valid actions) ==========
|
|
233
|
+
if (validActions.length > 0) {
|
|
234
|
+
const rightSection = Row({
|
|
235
|
+
alignItems: "center",
|
|
236
|
+
justifyContent: "flex-end",
|
|
237
|
+
paddingRight: paddingRight,
|
|
238
|
+
gap: actionsGap,
|
|
239
|
+
minWidth: 48,
|
|
240
|
+
flexShrink: 0,
|
|
241
|
+
backgroundColor: "transparent",
|
|
242
|
+
children: validActions,
|
|
243
|
+
});
|
|
244
|
+
appBar.appendChild(rightSection);
|
|
245
|
+
} else {
|
|
246
|
+
// Invisible placeholder maintains three-zone layout balance when there
|
|
247
|
+
// are no actions, keeping the title centered when centerTitle is true.
|
|
248
|
+
const emptyRight = Container({
|
|
249
|
+
minWidth: 48,
|
|
250
|
+
flexShrink: 0,
|
|
251
|
+
visibility: "hidden",
|
|
252
|
+
});
|
|
253
|
+
appBar.appendChild(emptyRight);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ========== HIDE-ON-SCROLL BEHAVIOUR ==========
|
|
257
|
+
// Attach a scroll listener that slides the bar off-screen when the user
|
|
258
|
+
// scrolls down past the threshold, and restores it on scroll-up.
|
|
259
|
+
if (hideOnScroll) {
|
|
260
|
+
let lastScrollY = 0;
|
|
261
|
+
const handleScroll = () => {
|
|
262
|
+
const currentScrollY = window.scrollY;
|
|
263
|
+
if (currentScrollY > lastScrollY && currentScrollY > scrollThreshold) {
|
|
264
|
+
appBar.style.transform = "translateY(-100%)";
|
|
265
|
+
appBar.style.opacity = "0";
|
|
266
|
+
} else if (currentScrollY < lastScrollY) {
|
|
267
|
+
appBar.style.transform = "translateY(0)";
|
|
268
|
+
appBar.style.opacity = "1";
|
|
269
|
+
}
|
|
270
|
+
lastScrollY = currentScrollY;
|
|
271
|
+
};
|
|
272
|
+
window.addEventListener("scroll", handleScroll);
|
|
273
|
+
// Store the remover so _cleanup can deregister the listener.
|
|
274
|
+
appBar._cleanupScroll = () =>
|
|
275
|
+
window.removeEventListener("scroll", handleScroll);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ========== PUBLIC API ==========
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Update the title text in place without rebuilding the bar.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} newTitle
|
|
284
|
+
*/
|
|
285
|
+
appBar.setTitle = (newTitle) => {
|
|
286
|
+
if (typeof newTitle === "string") {
|
|
287
|
+
const titleText = centerSection.querySelector(
|
|
288
|
+
"span:not(.material-icons)",
|
|
289
|
+
);
|
|
290
|
+
if (titleText) titleText.textContent = newTitle;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Swap the background color or gradient at runtime.
|
|
296
|
+
* Passing a string that contains the word "gradient" sets backgroundImage;
|
|
297
|
+
* otherwise it sets backgroundColor.
|
|
298
|
+
*
|
|
299
|
+
* @param {string} color - CSS color value or gradient string.
|
|
300
|
+
*/
|
|
301
|
+
appBar.setBackgroundColor = (color) => {
|
|
302
|
+
if (color && color.includes("gradient")) {
|
|
303
|
+
appBar.style.backgroundImage = color;
|
|
304
|
+
appBar.style.backgroundColor = "";
|
|
305
|
+
} else {
|
|
306
|
+
appBar.style.backgroundColor = color;
|
|
307
|
+
appBar.style.backgroundImage = "";
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/** Make the app bar visible. */
|
|
312
|
+
appBar.show = () => {
|
|
313
|
+
appBar.style.display = "flex";
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/** Hide the app bar (removes it from the layout flow). */
|
|
317
|
+
appBar.hide = () => {
|
|
318
|
+
appBar.style.display = "none";
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// Cleanup: deregister the scroll listener added by hideOnScroll.
|
|
322
|
+
const originalCleanup = appBar._cleanup;
|
|
323
|
+
appBar._cleanup = () => {
|
|
324
|
+
if (originalCleanup) originalCleanup();
|
|
325
|
+
if (appBar._cleanupScroll) appBar._cleanupScroll();
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
return appBar;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
export default AppBar;
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// navigations/BottomNavigation.js
|
|
2
|
+
import { WidgetFactory } from "../widget-factory/index.js";
|
|
3
|
+
import { Container } from "../widgets/Container.js";
|
|
4
|
+
import { Text } from "../widgets/Text.js";
|
|
5
|
+
import { Icon } from "../widgets/Icon.js";
|
|
6
|
+
import { colors } from "../utils/themes.js";
|
|
7
|
+
import { getCurrentPath, subscribe, goTo } from "./Router.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} BottomNavigationItem
|
|
11
|
+
* @property {string} icon - Material Icon name.
|
|
12
|
+
* @property {string} [label] - Display label shown below the icon.
|
|
13
|
+
* @property {string} [route] - Router path this tab navigates to.
|
|
14
|
+
* Defaults to `/<label.toLowerCase()>` when omitted.
|
|
15
|
+
* @property {Function} [onPress] - Additional callback fired when the tab is tapped.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} BottomNavigationProps
|
|
20
|
+
* @property {BottomNavigationItem[]} [items=[]] - Tab definitions.
|
|
21
|
+
* @property {number} [currentIndex=0] - Initially active tab index (non-router mode).
|
|
22
|
+
* @property {Function} [onTabChange] - Callback fired with the new index whenever the active tab changes.
|
|
23
|
+
* @property {string} [backgroundColor] - Background fill color of the bar.
|
|
24
|
+
* @property {string} [selectedColor] - Icon and label color for the active tab.
|
|
25
|
+
* @property {string} [unselectedColor] - Icon and label color for inactive tabs.
|
|
26
|
+
* @property {boolean} [showLabels=true] - Whether to render text labels below icons.
|
|
27
|
+
* @property {number} [iconSize=24] - Icon size in pixels.
|
|
28
|
+
* @property {number} [height=65] - Bar height in pixels.
|
|
29
|
+
* @property {number} [margin=0] - Uniform outer margin.
|
|
30
|
+
* @property {number} [marginTop=0] - Top margin (overrides margin).
|
|
31
|
+
* @property {number} [marginBottom=0] - Bottom margin (overrides margin).
|
|
32
|
+
* @property {number} [marginLeft=0] - Left margin (overrides margin).
|
|
33
|
+
* @property {number} [marginRight=0] - Right margin (overrides margin).
|
|
34
|
+
* @property {number|string} [borderRadius=0] - Corner radius.
|
|
35
|
+
* @property {number|string} [padding=0] - Inner padding.
|
|
36
|
+
* @property {boolean|string} [shadow=true] - Whether to render a shadow; pass a CSS string for a custom shadow.
|
|
37
|
+
* @property {number} [elevation=2] - Shadow depth (0–5), used when shadow is true.
|
|
38
|
+
* @property {boolean} [useRouter=true] - Sync the active tab with the router instead of tracking an internal index.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* BottomNavigation renders a bottom tab bar with icon and optional label tabs.
|
|
43
|
+
*
|
|
44
|
+
* By default (`useRouter=true`) it integrates with the Router module: tapping
|
|
45
|
+
* a tab calls `goTo(route)` and the active highlighting is driven entirely by
|
|
46
|
+
* the current route, keeping the bar in sync with back/forward navigation and
|
|
47
|
+
* programmatic route changes. Set `useRouter=false` to operate in standalone
|
|
48
|
+
* mode where the bar manages its own active index.
|
|
49
|
+
*
|
|
50
|
+
* @param {BottomNavigationProps} props
|
|
51
|
+
* @returns {HTMLElement} The `<nav>` element, augmented with:
|
|
52
|
+
* - `setActiveIndex(index: number)` — programmatically activate a tab.
|
|
53
|
+
* - `getActiveIndex(): number` — returns the currently active tab index.
|
|
54
|
+
* - `getCurrentRoute(): string|null` — returns the current router path (router mode only).
|
|
55
|
+
*/
|
|
56
|
+
export const BottomNavigation = (props) => {
|
|
57
|
+
const {
|
|
58
|
+
items = [],
|
|
59
|
+
currentIndex = 0,
|
|
60
|
+
onTabChange,
|
|
61
|
+
backgroundColor = colors.surface,
|
|
62
|
+
selectedColor = colors.primary,
|
|
63
|
+
unselectedColor = colors.textSecondary,
|
|
64
|
+
showLabels = true,
|
|
65
|
+
iconSize = 24,
|
|
66
|
+
height = 65,
|
|
67
|
+
margin = 0,
|
|
68
|
+
marginTop = 0,
|
|
69
|
+
marginBottom = 0,
|
|
70
|
+
marginLeft = 0,
|
|
71
|
+
marginRight = 0,
|
|
72
|
+
borderRadius = 0,
|
|
73
|
+
padding = 0,
|
|
74
|
+
shadow = true,
|
|
75
|
+
elevation = 2,
|
|
76
|
+
useRouter = true,
|
|
77
|
+
...rest
|
|
78
|
+
} = props;
|
|
79
|
+
|
|
80
|
+
let activeIndex = currentIndex;
|
|
81
|
+
const buttons = [];
|
|
82
|
+
let unsubscribe = null;
|
|
83
|
+
// Tracks whether the initial router sync has been performed so we don't
|
|
84
|
+
// override `activeIndex` from the router on subsequent renders.
|
|
85
|
+
let isRouterSynced = false;
|
|
86
|
+
|
|
87
|
+
// Resolve shorthand margin/padding/radius props.
|
|
88
|
+
const finalMarginTop = marginTop || margin;
|
|
89
|
+
const finalMarginBottom = marginBottom || margin;
|
|
90
|
+
const finalMarginLeft = marginLeft || margin;
|
|
91
|
+
const finalMarginRight = marginRight || margin;
|
|
92
|
+
const finalBorderRadius =
|
|
93
|
+
typeof borderRadius === "number" ? `${borderRadius}px` : borderRadius;
|
|
94
|
+
const finalPadding = typeof padding === "number" ? `${padding}px` : padding;
|
|
95
|
+
|
|
96
|
+
// Material Design elevation → box-shadow lookup table.
|
|
97
|
+
const shadows = {
|
|
98
|
+
0: "none",
|
|
99
|
+
1: "0 1px 3px rgba(0,0,0,0.12)",
|
|
100
|
+
2: "0 3px 6px rgba(0,0,0,0.16)",
|
|
101
|
+
3: "0 6px 12px rgba(0,0,0,0.2)",
|
|
102
|
+
4: "0 10px 20px rgba(0,0,0,0.25)",
|
|
103
|
+
5: "0 15px 30px rgba(0,0,0,0.3)",
|
|
104
|
+
};
|
|
105
|
+
const boxShadow = shadow === true ? shadows[elevation] : shadow || "none";
|
|
106
|
+
|
|
107
|
+
const container = WidgetFactory({
|
|
108
|
+
tag: "nav",
|
|
109
|
+
display: "flex",
|
|
110
|
+
width: `calc(100% - ${finalMarginLeft}px - ${finalMarginRight}px)`,
|
|
111
|
+
height: height,
|
|
112
|
+
backgroundColor: backgroundColor,
|
|
113
|
+
borderRadius: finalBorderRadius,
|
|
114
|
+
marginTop: finalMarginTop,
|
|
115
|
+
marginBottom: finalMarginBottom,
|
|
116
|
+
marginLeft: finalMarginLeft,
|
|
117
|
+
marginRight: finalMarginRight,
|
|
118
|
+
padding: finalPadding,
|
|
119
|
+
boxShadow: boxShadow,
|
|
120
|
+
flexShrink: 0,
|
|
121
|
+
...rest,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Update icon and label colors for the given active tab index.
|
|
126
|
+
* Used exclusively in non-router mode where the bar manages its own state.
|
|
127
|
+
*
|
|
128
|
+
* @param {number} index - The index of the newly active tab.
|
|
129
|
+
*/
|
|
130
|
+
const updateActive = (index) => {
|
|
131
|
+
buttons.forEach((btnData, i) => {
|
|
132
|
+
const isActive = i === index;
|
|
133
|
+
const icon = btnData.btn.querySelector(".material-icons");
|
|
134
|
+
const label = btnData.btn.querySelector(".bottom-nav-label");
|
|
135
|
+
if (icon) icon.style.color = isActive ? selectedColor : unselectedColor;
|
|
136
|
+
if (label) {
|
|
137
|
+
label.style.color = isActive ? selectedColor : unselectedColor;
|
|
138
|
+
label.style.fontWeight = isActive ? "500" : "400";
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
if (onTabChange) onTabChange(index);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Re-read the current router path and update icon/label colors to match.
|
|
146
|
+
* Called on every route change event in router mode.
|
|
147
|
+
*/
|
|
148
|
+
const updateColorsFromRouter = () => {
|
|
149
|
+
if (!useRouter) return;
|
|
150
|
+
|
|
151
|
+
const currentPath = getCurrentPath();
|
|
152
|
+
|
|
153
|
+
buttons.forEach((btnData, idx) => {
|
|
154
|
+
const isSelected = btnData.route === currentPath;
|
|
155
|
+
const color = isSelected ? selectedColor : unselectedColor;
|
|
156
|
+
|
|
157
|
+
if (btnData.icon) btnData.icon.style.color = color;
|
|
158
|
+
if (btnData.labelWidget) {
|
|
159
|
+
btnData.labelWidget.style.color = color;
|
|
160
|
+
btnData.labelWidget.style.fontWeight = isSelected ? "500" : "400";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (isSelected) activeIndex = idx;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (onTabChange) onTabChange(activeIndex);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// ========== BUILD TAB BUTTONS ==========
|
|
170
|
+
items.forEach((item, idx) => {
|
|
171
|
+
const itemRoute = item.route || `/${item.label?.toLowerCase()}`;
|
|
172
|
+
|
|
173
|
+
// Determine initial active state — derive from the router path on first
|
|
174
|
+
// render so the correct tab is highlighted when the page loads on a
|
|
175
|
+
// non-root route.
|
|
176
|
+
let isActive = activeIndex === idx;
|
|
177
|
+
if (useRouter && !isRouterSynced) {
|
|
178
|
+
const currentPath = getCurrentPath();
|
|
179
|
+
isActive = itemRoute === currentPath;
|
|
180
|
+
if (isActive) activeIndex = idx;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const btn = Container({
|
|
184
|
+
flex: 1,
|
|
185
|
+
display: "flex",
|
|
186
|
+
flexDirection: "column",
|
|
187
|
+
alignItems: "center",
|
|
188
|
+
justifyContent: "center",
|
|
189
|
+
gap: 4,
|
|
190
|
+
cursor: "pointer",
|
|
191
|
+
padding: "8px 0",
|
|
192
|
+
backgroundColor: "transparent",
|
|
193
|
+
onclick: () => {
|
|
194
|
+
if (useRouter) {
|
|
195
|
+
goTo(itemRoute);
|
|
196
|
+
if (item.onPress) item.onPress();
|
|
197
|
+
} else {
|
|
198
|
+
// In standalone mode, only update state when a different tab is tapped.
|
|
199
|
+
if (activeIndex !== idx) {
|
|
200
|
+
activeIndex = idx;
|
|
201
|
+
updateActive(idx);
|
|
202
|
+
if (item.onPress) item.onPress();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const icon = Icon({
|
|
209
|
+
name: item.icon,
|
|
210
|
+
size: iconSize,
|
|
211
|
+
color: isActive ? selectedColor : unselectedColor,
|
|
212
|
+
});
|
|
213
|
+
btn.appendChild(icon);
|
|
214
|
+
|
|
215
|
+
let labelWidget = null;
|
|
216
|
+
if (showLabels && item.label) {
|
|
217
|
+
labelWidget = Text({
|
|
218
|
+
value: item.label,
|
|
219
|
+
size: 11,
|
|
220
|
+
color: isActive ? selectedColor : unselectedColor,
|
|
221
|
+
fontWeight: isActive ? "500" : "400",
|
|
222
|
+
className: "bottom-nav-label",
|
|
223
|
+
});
|
|
224
|
+
btn.appendChild(labelWidget);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
buttons.push({
|
|
228
|
+
btn,
|
|
229
|
+
route: itemRoute,
|
|
230
|
+
icon,
|
|
231
|
+
// Store the label widget reference so updateColorsFromRouter can update
|
|
232
|
+
// it without querying the DOM.
|
|
233
|
+
labelWidget,
|
|
234
|
+
});
|
|
235
|
+
container.appendChild(btn);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Subscribe to router changes once all buttons are built.
|
|
239
|
+
if (useRouter) {
|
|
240
|
+
unsubscribe = subscribe(() => {
|
|
241
|
+
updateColorsFromRouter();
|
|
242
|
+
});
|
|
243
|
+
// Apply initial highlighting based on the current route.
|
|
244
|
+
updateColorsFromRouter();
|
|
245
|
+
isRouterSynced = true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ========== PUBLIC API ==========
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Programmatically activate a tab by index.
|
|
252
|
+
* In router mode, navigates to the tab's route. In standalone mode, updates
|
|
253
|
+
* the active index and calls `onPress` if provided.
|
|
254
|
+
*
|
|
255
|
+
* @param {number} index - Zero-based tab index.
|
|
256
|
+
*/
|
|
257
|
+
container.setActiveIndex = (index) => {
|
|
258
|
+
if (!useRouter) {
|
|
259
|
+
if (index >= 0 && index < items.length && activeIndex !== index) {
|
|
260
|
+
activeIndex = index;
|
|
261
|
+
updateActive(index);
|
|
262
|
+
if (items[index].onPress) items[index].onPress();
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
const route =
|
|
266
|
+
items[index]?.route || `/${items[index]?.label?.toLowerCase()}`;
|
|
267
|
+
if (route) goTo(route);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Returns the currently active tab index.
|
|
273
|
+
*
|
|
274
|
+
* @returns {number}
|
|
275
|
+
*/
|
|
276
|
+
container.getActiveIndex = () => activeIndex;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Returns the current router path (router mode only).
|
|
280
|
+
*
|
|
281
|
+
* @returns {string|null}
|
|
282
|
+
*/
|
|
283
|
+
container.getCurrentRoute = () => {
|
|
284
|
+
if (!useRouter) return null;
|
|
285
|
+
return getCurrentPath();
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Cleanup: unsubscribe from the router when the bar is removed from the DOM.
|
|
289
|
+
const originalCleanup = container._cleanup;
|
|
290
|
+
container._cleanup = () => {
|
|
291
|
+
if (unsubscribe) unsubscribe();
|
|
292
|
+
if (originalCleanup) originalCleanup();
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
return container;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export default BottomNavigation;
|