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,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file ProgressBar.js
|
|
3
|
+
* @description Linear progress bar (determinate/indeterminate) using shared keyframes.
|
|
4
|
+
*/
|
|
5
|
+
import { WidgetFactory } from "../widget-factory/index.js";
|
|
6
|
+
import { colors } from "../utils/themes.js";
|
|
7
|
+
import { injectKeyframes } from "../utils/styleInjector.js";
|
|
8
|
+
|
|
9
|
+
export const ProgressBar = (props) => {
|
|
10
|
+
const {
|
|
11
|
+
value = 0,
|
|
12
|
+
max = 100,
|
|
13
|
+
height = 8,
|
|
14
|
+
width = "100%",
|
|
15
|
+
color = colors.primary,
|
|
16
|
+
backgroundColor = colors.border,
|
|
17
|
+
borderRadius,
|
|
18
|
+
label,
|
|
19
|
+
showValue = false,
|
|
20
|
+
valuePosition = "right",
|
|
21
|
+
indeterminate = false,
|
|
22
|
+
striped = false,
|
|
23
|
+
animatedStripes = false,
|
|
24
|
+
...rest
|
|
25
|
+
} = props;
|
|
26
|
+
|
|
27
|
+
let progressValue = Math.min(Math.max(value, 0), max);
|
|
28
|
+
let percentage = (progressValue / max) * 100;
|
|
29
|
+
const finalBorderRadius =
|
|
30
|
+
borderRadius !== undefined ? borderRadius : height / 2;
|
|
31
|
+
const isColumn = valuePosition === "top" || valuePosition === "bottom";
|
|
32
|
+
|
|
33
|
+
// Createste fill bar
|
|
34
|
+
const fill = WidgetFactory({
|
|
35
|
+
width: indeterminate ? "100%" : `${percentage}%`,
|
|
36
|
+
height: "100%",
|
|
37
|
+
backgroundColor: color,
|
|
38
|
+
borderRadius:
|
|
39
|
+
typeof finalBorderRadius === "number"
|
|
40
|
+
? `${finalBorderRadius}px`
|
|
41
|
+
: finalBorderRadius,
|
|
42
|
+
transition: indeterminate ? "none" : "width 0.3s ease",
|
|
43
|
+
position: "relative",
|
|
44
|
+
...(striped || indeterminate
|
|
45
|
+
? {
|
|
46
|
+
backgroundImage: `repeating-linear-gradient(45deg, rgba(255,255,255,0.15) 0px, rgba(255,255,255,0.15) 10px, transparent 10px, transparent 20px)`,
|
|
47
|
+
backgroundSize: "28px 28px",
|
|
48
|
+
}
|
|
49
|
+
: {}),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Animated stripes
|
|
53
|
+
if (animatedStripes || indeterminate) {
|
|
54
|
+
fill.style.animation = `progress-stripes ${indeterminate ? "1.5s" : "0.5s"} linear infinite`;
|
|
55
|
+
injectKeyframes(
|
|
56
|
+
"progress-stripes",
|
|
57
|
+
"0% { background-position: 0 0; } 100% { background-position: 28px 0; }",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Indeterminate animation
|
|
62
|
+
if (indeterminate) {
|
|
63
|
+
fill.style.width = "50%";
|
|
64
|
+
fill.style.transform = "translateX(-100%)";
|
|
65
|
+
fill.style.animation = "indeterminate-progress 1.5s ease-in-out infinite";
|
|
66
|
+
injectKeyframes(
|
|
67
|
+
"indeterminate-progress",
|
|
68
|
+
"0% { transform: translateX(-100%); } 50% { transform: translateX(0%); } 100% { transform: translateX(100%); }",
|
|
69
|
+
"indeterminate-progress-style",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Track background
|
|
74
|
+
const track = WidgetFactory({
|
|
75
|
+
flex: 1,
|
|
76
|
+
height: typeof height === "number" ? `${height}px` : height,
|
|
77
|
+
backgroundColor: backgroundColor,
|
|
78
|
+
borderRadius:
|
|
79
|
+
typeof finalBorderRadius === "number"
|
|
80
|
+
? `${finalBorderRadius}px`
|
|
81
|
+
: finalBorderRadius,
|
|
82
|
+
overflow: "hidden",
|
|
83
|
+
position: "relative",
|
|
84
|
+
child: fill,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Build children array for main container
|
|
88
|
+
const children = [];
|
|
89
|
+
|
|
90
|
+
// Label (left or top)
|
|
91
|
+
if (label && (valuePosition === "left" || valuePosition === "top")) {
|
|
92
|
+
children.push(
|
|
93
|
+
WidgetFactory({
|
|
94
|
+
tag: "span",
|
|
95
|
+
textContent: label,
|
|
96
|
+
fontSize: "12px",
|
|
97
|
+
color: colors.textSecondary,
|
|
98
|
+
whiteSpace: "nowrap",
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
children.push(track);
|
|
104
|
+
|
|
105
|
+
// Label (right or bottom)
|
|
106
|
+
if (label && (valuePosition === "right" || valuePosition === "bottom")) {
|
|
107
|
+
children.push(
|
|
108
|
+
WidgetFactory({
|
|
109
|
+
tag: "span",
|
|
110
|
+
textContent: label,
|
|
111
|
+
fontSize: "12px",
|
|
112
|
+
color: colors.textSecondary,
|
|
113
|
+
whiteSpace: "nowrap",
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Value percentage
|
|
119
|
+
if (showValue && !indeterminate) {
|
|
120
|
+
const valueEl = WidgetFactory({
|
|
121
|
+
tag: "span",
|
|
122
|
+
textContent: `${Math.round(percentage)}%`,
|
|
123
|
+
fontSize: "12px",
|
|
124
|
+
color: colors.textSecondary,
|
|
125
|
+
fontWeight: "500",
|
|
126
|
+
whiteSpace: "nowrap",
|
|
127
|
+
minWidth: "40px",
|
|
128
|
+
textAlign: "right",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (valuePosition === "left" || valuePosition === "top") {
|
|
132
|
+
children.unshift(valueEl);
|
|
133
|
+
} else {
|
|
134
|
+
children.push(valueEl);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Main container
|
|
139
|
+
const container = WidgetFactory({
|
|
140
|
+
widgetName: "ProgressBar",
|
|
141
|
+
display: "flex",
|
|
142
|
+
flexDirection: isColumn ? "column" : "row",
|
|
143
|
+
alignItems: "center",
|
|
144
|
+
gap: "8px",
|
|
145
|
+
width: typeof width === "number" ? `${width}px` : width,
|
|
146
|
+
children: children,
|
|
147
|
+
...rest,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Public methods
|
|
151
|
+
const updateProgress = (newValue) => {
|
|
152
|
+
if (indeterminate) return;
|
|
153
|
+
progressValue = Math.min(Math.max(newValue, 0), max);
|
|
154
|
+
percentage = (progressValue / max) * 100;
|
|
155
|
+
fill.style.width = `${percentage}%`;
|
|
156
|
+
|
|
157
|
+
if (showValue) {
|
|
158
|
+
const valueEl = container.querySelector("span:last-child");
|
|
159
|
+
if (valueEl && valueEl !== label) {
|
|
160
|
+
valueEl.textContent = `${Math.round(percentage)}%`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
Object.defineProperty(container, "value", {
|
|
166
|
+
get: () => progressValue,
|
|
167
|
+
set: (newVal) => updateProgress(newVal),
|
|
168
|
+
enumerable: true,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
container.updateProgress = updateProgress;
|
|
172
|
+
container.getValue = () => progressValue;
|
|
173
|
+
container.setValue = updateProgress;
|
|
174
|
+
|
|
175
|
+
return container;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export default ProgressBar;
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file QRCode.js
|
|
3
|
+
* @description QR code widget that generates real, scannable QR codes entirely
|
|
4
|
+
* in-browser with zero external dependencies. Implements QR Code Model 2
|
|
5
|
+
* (versions 1–10, byte mode, error correction L/M/Q/H) using:
|
|
6
|
+
* - ISO 18004-compliant data encoding (byte mode)
|
|
7
|
+
* - Reed-Solomon error correction
|
|
8
|
+
* - Standard function patterns (finder, timing, alignment, format info)
|
|
9
|
+
* - Data masking (all 8 masks evaluated, best chosen by penalty score)
|
|
10
|
+
* - Column-pair zigzag data placement
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { WidgetFactory } from "../widget-factory/index.js";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// QR constants
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/** Error correction capacity tables [version][ecLevel] = { totalCodewords, ecCodewordsPerBlock, blocks } */
|
|
20
|
+
const EC_TABLE = {
|
|
21
|
+
// [totalDataCodewords, ecPerBlock, blocks]
|
|
22
|
+
1: { L: [19, 7, 1], M: [16, 10, 1], Q: [13, 13, 1], H: [9, 17, 1] },
|
|
23
|
+
2: { L: [34, 10, 1], M: [28, 16, 1], Q: [22, 22, 1], H: [16, 28, 1] },
|
|
24
|
+
3: { L: [55, 15, 1], M: [44, 26, 1], Q: [34, 18, 2], H: [26, 22, 2] },
|
|
25
|
+
4: { L: [80, 20, 1], M: [64, 18, 2], Q: [48, 26, 2], H: [36, 16, 4] },
|
|
26
|
+
5: { L: [108,26, 1], M: [86, 24, 2], Q: [62, 18, 4], H: [46, 22, 2] }, // simplified
|
|
27
|
+
6: { L: [136,18, 2], M: [108,16, 4], Q: [76, 24, 4], H: [60, 28, 4] },
|
|
28
|
+
7: { L: [156,20, 2], M: [124,18, 4], Q: [88, 18, 6], H: [66, 26, 4] },
|
|
29
|
+
8: { L: [194,24, 2], M: [154,22, 2], Q: [110,22, 4], H: [86, 26, 4] },
|
|
30
|
+
9: { L: [232,30, 2], M: [182,22, 3], Q: [132,20, 4], H: [100,24, 4] },
|
|
31
|
+
10: { L: [274,18, 4], M: [216,26, 4], Q: [154,24, 6], H: [122,28, 6] },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Alignment pattern center positions per version (version 1 has none) */
|
|
35
|
+
const ALIGN_POS = {
|
|
36
|
+
1: [], 2: [6,18], 3: [6,22], 4: [6,26], 5: [6,30],
|
|
37
|
+
6: [6,34], 7: [6,22,38], 8: [6,24,42], 9: [6,26,46], 10: [6,28,50],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** GF(256) tables for Reed-Solomon (generator polynomial x^8+x^4+x^3+x^2+1 = 0x11D) */
|
|
41
|
+
const GF_EXP = new Uint8Array(512);
|
|
42
|
+
const GF_LOG = new Uint8Array(256);
|
|
43
|
+
(function buildGF() {
|
|
44
|
+
let x = 1;
|
|
45
|
+
for (let i = 0; i < 255; i++) {
|
|
46
|
+
GF_EXP[i] = x;
|
|
47
|
+
GF_LOG[x] = i;
|
|
48
|
+
x <<= 1;
|
|
49
|
+
if (x & 0x100) x ^= 0x11D;
|
|
50
|
+
}
|
|
51
|
+
for (let i = 255; i < 512; i++) GF_EXP[i] = GF_EXP[i - 255];
|
|
52
|
+
})();
|
|
53
|
+
|
|
54
|
+
const gfMul = (a, b) => (a === 0 || b === 0) ? 0 : GF_EXP[GF_LOG[a] + GF_LOG[b]];
|
|
55
|
+
|
|
56
|
+
/** Generate Reed-Solomon generator polynomial of degree `n` */
|
|
57
|
+
function rsGenerator(n) {
|
|
58
|
+
let g = [1];
|
|
59
|
+
for (let i = 0; i < n; i++) {
|
|
60
|
+
const factor = [1, GF_EXP[i]];
|
|
61
|
+
const result = new Array(g.length + factor.length - 1).fill(0);
|
|
62
|
+
for (let j = 0; j < g.length; j++)
|
|
63
|
+
for (let k = 0; k < factor.length; k++)
|
|
64
|
+
result[j + k] ^= gfMul(g[j], factor[k]);
|
|
65
|
+
g = result;
|
|
66
|
+
}
|
|
67
|
+
return g;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Compute `ecCount` Reed-Solomon error correction codewords for `data` */
|
|
71
|
+
function rsEncode(data, ecCount) {
|
|
72
|
+
const gen = rsGenerator(ecCount);
|
|
73
|
+
const msg = [...data, ...new Array(ecCount).fill(0)];
|
|
74
|
+
for (let i = 0; i < data.length; i++) {
|
|
75
|
+
const coeff = msg[i];
|
|
76
|
+
if (coeff !== 0) {
|
|
77
|
+
for (let j = 1; j < gen.length; j++) {
|
|
78
|
+
msg[i + j] ^= gfMul(gen[j], coeff);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return msg.slice(data.length);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// QR matrix builder
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build a complete, scannable QR code matrix.
|
|
91
|
+
* @param {string} text - The text to encode.
|
|
92
|
+
* @param {'L'|'M'|'Q'|'H'} ecLevel - Error correction level.
|
|
93
|
+
* @returns {{ matrix: number[][], size: number }}
|
|
94
|
+
*/
|
|
95
|
+
function buildQRMatrix(text, ecLevel = "M") {
|
|
96
|
+
// 1. Encode text as UTF-8 bytes
|
|
97
|
+
const encoder = new TextEncoder();
|
|
98
|
+
const bytes = encoder.encode(text);
|
|
99
|
+
|
|
100
|
+
// 2. Choose smallest version that fits
|
|
101
|
+
let version = 1;
|
|
102
|
+
for (; version <= 10; version++) {
|
|
103
|
+
const [dataCodewords] = EC_TABLE[version][ecLevel];
|
|
104
|
+
// Byte mode header: 4 (mode) + 8 (char count) + 8*N bits, rounded to codewords
|
|
105
|
+
const totalBits = 4 + 8 + bytes.length * 8;
|
|
106
|
+
if (Math.ceil(totalBits / 8) <= dataCodewords) break;
|
|
107
|
+
}
|
|
108
|
+
if (version > 10) version = 10; // clamp; long text will be truncated
|
|
109
|
+
|
|
110
|
+
const size = version * 4 + 17;
|
|
111
|
+
const [dataCodewords, ecPerBlock, numBlocks] = EC_TABLE[version][ecLevel];
|
|
112
|
+
|
|
113
|
+
// 3. Build bit stream (byte mode)
|
|
114
|
+
const bits = [];
|
|
115
|
+
const pushBits = (val, len) => {
|
|
116
|
+
for (let i = len - 1; i >= 0; i--) bits.push((val >> i) & 1);
|
|
117
|
+
};
|
|
118
|
+
pushBits(0b0100, 4); // mode indicator: byte
|
|
119
|
+
pushBits(bytes.length, 8); // character count
|
|
120
|
+
for (const b of bytes) pushBits(b, 8);
|
|
121
|
+
// Terminator
|
|
122
|
+
for (let i = 0; i < 4 && bits.length < dataCodewords * 8; i++) bits.push(0);
|
|
123
|
+
// Pad to byte boundary
|
|
124
|
+
while (bits.length % 8 !== 0) bits.push(0);
|
|
125
|
+
// Pad codewords
|
|
126
|
+
const padBytes = [0xEC, 0x11];
|
|
127
|
+
let padIdx = 0;
|
|
128
|
+
while (bits.length < dataCodewords * 8) {
|
|
129
|
+
pushBits(padBytes[padIdx++ % 2], 8);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 4. Convert bits to data codewords
|
|
133
|
+
const dataCW = [];
|
|
134
|
+
for (let i = 0; i < bits.length; i += 8) {
|
|
135
|
+
let b = 0;
|
|
136
|
+
for (let j = 0; j < 8; j++) b = (b << 1) | (bits[i + j] || 0);
|
|
137
|
+
dataCW.push(b);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 5. Split into blocks and compute EC codewords
|
|
141
|
+
const blockSize = Math.floor(dataCodewords / numBlocks);
|
|
142
|
+
const extraBlocks = dataCodewords - blockSize * numBlocks;
|
|
143
|
+
const dataBlocks = [];
|
|
144
|
+
const ecBlocks = [];
|
|
145
|
+
let offset = 0;
|
|
146
|
+
for (let b = 0; b < numBlocks; b++) {
|
|
147
|
+
const len = b < numBlocks - extraBlocks ? blockSize : blockSize + 1;
|
|
148
|
+
const block = dataCW.slice(offset, offset + len);
|
|
149
|
+
offset += len;
|
|
150
|
+
dataBlocks.push(block);
|
|
151
|
+
ecBlocks.push(rsEncode(block, ecPerBlock));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 6. Interleave data codewords, then EC codewords
|
|
155
|
+
const finalCW = [];
|
|
156
|
+
const maxData = Math.max(...dataBlocks.map(b => b.length));
|
|
157
|
+
for (let i = 0; i < maxData; i++)
|
|
158
|
+
for (const block of dataBlocks) if (i < block.length) finalCW.push(block[i]);
|
|
159
|
+
for (let i = 0; i < ecPerBlock; i++)
|
|
160
|
+
for (const block of ecBlocks) finalCW.push(block[i]);
|
|
161
|
+
|
|
162
|
+
// 7. Build final bit sequence
|
|
163
|
+
const finalBits = [];
|
|
164
|
+
for (const cw of finalCW) pushBits(cw, 8);
|
|
165
|
+
// Remainder bits (version-dependent, versions 2–6 need 7)
|
|
166
|
+
const remainderBitsCount = version >= 2 && version <= 6 ? 7 : 0;
|
|
167
|
+
for (let i = 0; i < remainderBitsCount; i++) finalBits.push(0);
|
|
168
|
+
|
|
169
|
+
// 8. Initialize matrix with -1 (unfilled)
|
|
170
|
+
const mat = Array.from({ length: size }, () => new Int8Array(size).fill(-1));
|
|
171
|
+
const reserved = Array.from({ length: size }, () => new Uint8Array(size));
|
|
172
|
+
|
|
173
|
+
const setModule = (r, c, val) => { mat[r][c] = val; reserved[r][c] = 1; };
|
|
174
|
+
|
|
175
|
+
// 9. Place function patterns
|
|
176
|
+
|
|
177
|
+
// Finder patterns (7×7) + separators
|
|
178
|
+
const addFinder = (row, col) => {
|
|
179
|
+
for (let r = -1; r <= 7; r++) {
|
|
180
|
+
for (let c = -1; c <= 7; c++) {
|
|
181
|
+
const nr = row + r, nc = col + c;
|
|
182
|
+
if (nr < 0 || nr >= size || nc < 0 || nc >= size) continue;
|
|
183
|
+
const isInner = r >= 0 && r <= 6 && c >= 0 && c <= 6;
|
|
184
|
+
let val = 0;
|
|
185
|
+
if (isInner) {
|
|
186
|
+
val = (r === 0 || r === 6 || c === 0 || c === 6) ? 1
|
|
187
|
+
: (r >= 2 && r <= 4 && c >= 2 && c <= 4) ? 1 : 0;
|
|
188
|
+
}
|
|
189
|
+
setModule(nr, nc, val);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
addFinder(0, 0);
|
|
194
|
+
addFinder(0, size - 7);
|
|
195
|
+
addFinder(size - 7, 0);
|
|
196
|
+
|
|
197
|
+
// Timing patterns
|
|
198
|
+
for (let i = 8; i < size - 8; i++) {
|
|
199
|
+
setModule(6, i, i % 2 === 0 ? 1 : 0);
|
|
200
|
+
setModule(i, 6, i % 2 === 0 ? 1 : 0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Dark module
|
|
204
|
+
setModule(size - 8, 8, 1);
|
|
205
|
+
|
|
206
|
+
// Alignment patterns
|
|
207
|
+
const ap = ALIGN_POS[version];
|
|
208
|
+
for (let ai = 0; ai < ap.length; ai++) {
|
|
209
|
+
for (let aj = 0; aj < ap.length; aj++) {
|
|
210
|
+
const r = ap[ai], c = ap[aj];
|
|
211
|
+
if (reserved[r][c]) continue; // skip if overlaps finder
|
|
212
|
+
for (let dr = -2; dr <= 2; dr++) {
|
|
213
|
+
for (let dc = -2; dc <= 2; dc++) {
|
|
214
|
+
const v = (Math.abs(dr) === 2 || Math.abs(dc) === 2) ? 1
|
|
215
|
+
: (dr === 0 && dc === 0) ? 1 : 0;
|
|
216
|
+
setModule(r + dr, c + dc, v);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Reserve format info areas (filled later)
|
|
223
|
+
for (let i = 0; i < 9; i++) {
|
|
224
|
+
reserved[8][i] = 1; reserved[i][8] = 1;
|
|
225
|
+
}
|
|
226
|
+
for (let i = size - 8; i < size; i++) {
|
|
227
|
+
reserved[8][i] = 1; reserved[i][8] = 1;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 10. Place data bits using zigzag column-pair scan
|
|
231
|
+
let bitIdx = 0;
|
|
232
|
+
let goingUp = true;
|
|
233
|
+
let col = size - 1;
|
|
234
|
+
while (col > 0) {
|
|
235
|
+
if (col === 6) col--; // skip timing column
|
|
236
|
+
for (let rowOffset = 0; rowOffset < size; rowOffset++) {
|
|
237
|
+
const row = goingUp ? size - 1 - rowOffset : rowOffset;
|
|
238
|
+
for (let c = col; c >= col - 1; c--) {
|
|
239
|
+
if (!reserved[row][c]) {
|
|
240
|
+
mat[row][c] = bitIdx < finalBits.length ? finalBits[bitIdx++] : 0;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
goingUp = !goingUp;
|
|
245
|
+
col -= 2;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 11. Apply best mask and write format info
|
|
249
|
+
const ecBits = { L: 0b01, M: 0b00, Q: 0b11, H: 0b10 }[ecLevel];
|
|
250
|
+
|
|
251
|
+
const applyMask = (maskId, m) => {
|
|
252
|
+
const fn = [
|
|
253
|
+
(r, c) => (r + c) % 2 === 0,
|
|
254
|
+
(r, c) => r % 2 === 0,
|
|
255
|
+
(r, c) => c % 3 === 0,
|
|
256
|
+
(r, c) => (r + c) % 3 === 0,
|
|
257
|
+
(r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0,
|
|
258
|
+
(r, c) => ((r * c) % 2 + (r * c) % 3) === 0,
|
|
259
|
+
(r, c) => ((r * c) % 2 + (r * c) % 3) % 2 === 0,
|
|
260
|
+
(r, c) => ((r + c) % 2 + (r * c) % 3) % 2 === 0,
|
|
261
|
+
][maskId];
|
|
262
|
+
for (let r = 0; r < size; r++)
|
|
263
|
+
for (let c = 0; c < size; c++)
|
|
264
|
+
if (!reserved[r][c] && fn(r, c)) m[r][c] ^= 1;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const penaltyScore = (m) => {
|
|
268
|
+
let score = 0;
|
|
269
|
+
// Rule 1: 5+ in a row
|
|
270
|
+
for (let r = 0; r < size; r++) {
|
|
271
|
+
for (let isCol = 0; isCol < 2; isCol++) {
|
|
272
|
+
let run = 1;
|
|
273
|
+
for (let i = 1; i < size; i++) {
|
|
274
|
+
const prev = isCol ? m[i-1][r] : m[r][i-1];
|
|
275
|
+
const cur = isCol ? m[i][r] : m[r][i];
|
|
276
|
+
if (cur === prev) { run++; if (run === 5) score += 3; else if (run > 5) score++; }
|
|
277
|
+
else run = 1;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Rule 2: 2×2 blocks
|
|
282
|
+
for (let r = 0; r < size - 1; r++)
|
|
283
|
+
for (let c = 0; c < size - 1; c++)
|
|
284
|
+
if (m[r][c] === m[r][c+1] && m[r][c] === m[r+1][c] && m[r][c] === m[r+1][c+1])
|
|
285
|
+
score += 3;
|
|
286
|
+
// Rule 3: finder-like patterns
|
|
287
|
+
const pat1 = [1,0,1,1,1,0,1,0,0,0,0], pat2 = [0,0,0,0,1,0,1,1,1,0,1];
|
|
288
|
+
for (let r = 0; r < size; r++) {
|
|
289
|
+
for (let c = 0; c <= size - 11; c++) {
|
|
290
|
+
let m1 = true, m2 = true;
|
|
291
|
+
for (let k = 0; k < 11; k++) {
|
|
292
|
+
if (m[r][c+k] !== pat1[k]) m1 = false;
|
|
293
|
+
if (m[r][c+k] !== pat2[k]) m2 = false;
|
|
294
|
+
}
|
|
295
|
+
if (m1 || m2) score += 40;
|
|
296
|
+
m1 = true; m2 = true;
|
|
297
|
+
for (let k = 0; k < 11; k++) {
|
|
298
|
+
if (m[c+k][r] !== pat1[k]) m1 = false;
|
|
299
|
+
if (m[c+k][r] !== pat2[k]) m2 = false;
|
|
300
|
+
}
|
|
301
|
+
if (m1 || m2) score += 40;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Rule 4: dark/light ratio
|
|
305
|
+
let dark = 0;
|
|
306
|
+
for (let r = 0; r < size; r++) for (let c = 0; c < size; c++) if (m[r][c] === 1) dark++;
|
|
307
|
+
const pct = (dark / (size * size)) * 100;
|
|
308
|
+
const prev5 = Math.floor(Math.abs(pct - 50) / 5) * 5;
|
|
309
|
+
score += Math.min(prev5, prev5 + 5) * 2;
|
|
310
|
+
return score;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// Evaluate all 8 masks, pick lowest penalty
|
|
314
|
+
let bestMask = 0, bestScore = Infinity;
|
|
315
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
316
|
+
const copy = mat.map(r => Int8Array.from(r));
|
|
317
|
+
applyMask(mask, copy);
|
|
318
|
+
const s = penaltyScore(copy);
|
|
319
|
+
if (s < bestScore) { bestScore = s; bestMask = mask; }
|
|
320
|
+
}
|
|
321
|
+
applyMask(bestMask, mat);
|
|
322
|
+
|
|
323
|
+
// Write format information
|
|
324
|
+
const writeFormat = (bits15) => {
|
|
325
|
+
const fmtPositions = [
|
|
326
|
+
[[8,0],[8,1],[8,2],[8,3],[8,4],[8,5],[8,7],[8,8],[7,8],[5,8],[4,8],[3,8],[2,8],[1,8],[0,8]],
|
|
327
|
+
[[size-1,8],[size-2,8],[size-3,8],[size-4,8],[size-5,8],[size-6,8],[size-7,8],[8,size-8],[8,size-7],[8,size-6],[8,size-5],[8,size-4],[8,size-3],[8,size-2],[8,size-1]],
|
|
328
|
+
];
|
|
329
|
+
for (let copy = 0; copy < 2; copy++)
|
|
330
|
+
for (let i = 0; i < 15; i++)
|
|
331
|
+
mat[fmtPositions[copy][i][0]][fmtPositions[copy][i][1]] = (bits15 >> (14 - i)) & 1;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// Format info: ec(2 bits) + mask(3 bits), BCH(10 bits), XOR 101010000010010
|
|
335
|
+
let fmtData = (ecBits << 3) | bestMask;
|
|
336
|
+
let fmtBCH = fmtData << 10;
|
|
337
|
+
const poly = 0b10100110111;
|
|
338
|
+
for (let i = 14; i >= 10; i--)
|
|
339
|
+
if ((fmtBCH >> i) & 1) fmtBCH ^= poly << (i - 10);
|
|
340
|
+
writeFormat(((fmtData << 10) | fmtBCH) ^ 0b101010000010010);
|
|
341
|
+
|
|
342
|
+
return { matrix: mat, size };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// Widget
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Creates a QRCode widget that renders a real, scannable QR code on a canvas.
|
|
351
|
+
*
|
|
352
|
+
* @param {Object} props
|
|
353
|
+
* @param {string} props.value - The text or URL to encode. Required.
|
|
354
|
+
* @param {number} [props.size=200] - Canvas size in pixels (square).
|
|
355
|
+
* @param {string} [props.bgColor=#ffffff] - Background (light module) color.
|
|
356
|
+
* @param {string} [props.fgColor=#000000] - Foreground (dark module) color.
|
|
357
|
+
* @param {'L'|'M'|'Q'|'H'} [props.errorCorrection=M] - Error correction level.
|
|
358
|
+
* L=7%, M=15%, Q=25%, H=30% of codewords recoverable.
|
|
359
|
+
* @param {number} [props.margin=4] - Quiet zone in modules around the QR.
|
|
360
|
+
* @returns {HTMLElement|null} The canvas container element, or `null` when
|
|
361
|
+
* `value` is empty (nothing to render).
|
|
362
|
+
*/
|
|
363
|
+
export const QRCode = (props) => {
|
|
364
|
+
const {
|
|
365
|
+
value = "",
|
|
366
|
+
size = 200,
|
|
367
|
+
bgColor = "#ffffff",
|
|
368
|
+
fgColor = "#000000",
|
|
369
|
+
errorCorrection = "M",
|
|
370
|
+
margin = 4,
|
|
371
|
+
...rest
|
|
372
|
+
} = props;
|
|
373
|
+
|
|
374
|
+
if (!value) return null;
|
|
375
|
+
|
|
376
|
+
let currentValue = value;
|
|
377
|
+
let canvasRef = null;
|
|
378
|
+
|
|
379
|
+
const drawQR = () => {
|
|
380
|
+
if (!canvasRef) return;
|
|
381
|
+
const canvas = canvasRef;
|
|
382
|
+
const ctx = canvas.getContext("2d");
|
|
383
|
+
|
|
384
|
+
let result;
|
|
385
|
+
try {
|
|
386
|
+
result = buildQRMatrix(currentValue, errorCorrection);
|
|
387
|
+
} catch (e) {
|
|
388
|
+
// Fallback: show error text
|
|
389
|
+
canvas.width = size;
|
|
390
|
+
canvas.height = size;
|
|
391
|
+
ctx.fillStyle = bgColor;
|
|
392
|
+
ctx.fillRect(0, 0, size, size);
|
|
393
|
+
ctx.fillStyle = "#cc0000";
|
|
394
|
+
ctx.font = "12px sans-serif";
|
|
395
|
+
ctx.textAlign = "center";
|
|
396
|
+
ctx.fillText("QR error", size / 2, size / 2);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const { matrix, size: matSize } = result;
|
|
401
|
+
const totalModules = matSize + margin * 2;
|
|
402
|
+
const cellSize = size / totalModules;
|
|
403
|
+
|
|
404
|
+
canvas.width = size;
|
|
405
|
+
canvas.height = size;
|
|
406
|
+
|
|
407
|
+
ctx.fillStyle = bgColor;
|
|
408
|
+
ctx.fillRect(0, 0, size, size);
|
|
409
|
+
|
|
410
|
+
ctx.fillStyle = fgColor;
|
|
411
|
+
for (let row = 0; row < matSize; row++) {
|
|
412
|
+
for (let col = 0; col < matSize; col++) {
|
|
413
|
+
if (matrix[row][col] === 1) {
|
|
414
|
+
const x = (col + margin) * cellSize;
|
|
415
|
+
const y = (row + margin) * cellSize;
|
|
416
|
+
ctx.fillRect(
|
|
417
|
+
Math.round(x),
|
|
418
|
+
Math.round(y),
|
|
419
|
+
Math.ceil(cellSize),
|
|
420
|
+
Math.ceil(cellSize)
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const canvas = document.createElement("canvas");
|
|
428
|
+
canvas.width = size;
|
|
429
|
+
canvas.height = size;
|
|
430
|
+
canvas.style.width = `${size}px`;
|
|
431
|
+
canvas.style.height = `${size}px`;
|
|
432
|
+
canvas.style.display = "block";
|
|
433
|
+
canvasRef = canvas;
|
|
434
|
+
|
|
435
|
+
// Draw on next tick so the canvas is in the DOM
|
|
436
|
+
setTimeout(() => drawQR(), 0);
|
|
437
|
+
|
|
438
|
+
const container = WidgetFactory({
|
|
439
|
+
widgetName: "QRCode",
|
|
440
|
+
display: "inline-block",
|
|
441
|
+
...rest,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
container.appendChild(canvas);
|
|
445
|
+
|
|
446
|
+
/** Update the encoded value and redraw */
|
|
447
|
+
container.updateValue = (newValue) => {
|
|
448
|
+
currentValue = newValue;
|
|
449
|
+
drawQR();
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
/** Trigger a PNG download of the current QR code */
|
|
453
|
+
container.download = (filename = "qrcode.png") => {
|
|
454
|
+
const link = document.createElement("a");
|
|
455
|
+
link.download = filename;
|
|
456
|
+
link.href = canvas.toDataURL("image/png");
|
|
457
|
+
link.click();
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
container.getDataURL = () => canvas.toDataURL("image/png");
|
|
461
|
+
container.getCanvas = () => canvas;
|
|
462
|
+
container.getValue = () => currentValue;
|
|
463
|
+
|
|
464
|
+
return container;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
export default QRCode;
|