flet-box 1.1.2 → 1.1.3
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/AGENTS_FOR_CODE_GEN.md +284 -0
- package/CONTRIBUTING.md +238 -0
- package/README.md +2 -2
- package/bin/cli.js +88 -1
- package/bin/commands/auditFix.js +181 -27
- package/bin/commands/checkApi.js +14 -3
- package/bin/commands/checkStructure.js +368 -0
- package/bin/commands/checkSyntax.js +293 -0
- package/bin/commands/checkUi.js +666 -14
- package/bin/commands/create.js +14 -0
- package/bin/commands/createBundle.js +2 -2
- package/bin/commands/metrics.js +448 -0
- package/bin/commands/release.js +18 -0
- package/bin/commands/report.js +849 -0
- package/bin/commands/tools.js +76 -0
- package/bin/commands/unpack.js +39 -11
- package/bin/commands/verify.js +879 -0
- package/bin/mcp.js +280 -0
- package/bin/utils/contract.js +27 -0
- package/bin/utils/dtsProps.js +5 -1
- package/bin/utils/migrationGuide.js +146 -0
- package/bin/utils/moduleExports.js +9 -5
- package/bin/utils/templates.js +225 -18
- package/bin/utils/toolkit.js +191 -0
- package/docs/README.md +5 -1
- package/docs/arquitectura.md +1 -1
- package/docs/cli/README.md +168 -2
- package/docs/guides/README.md +6 -0
- package/docs/guides/accessibility.md +2 -1
- package/docs/guides/ai-toolkit.md +166 -0
- package/docs/guides/compatibility.md +1 -1
- package/docs/guides/migration.md +92 -0
- package/docs/guides/minification-and-protection.md +2 -1
- package/docs/guides/mobile-and-platforms.md +1 -1
- package/docs/guides/online-updates.md +2 -0
- package/docs/server/deployment.md +1 -1
- package/docs/tools/styling.md +2 -2
- package/docs/tools/theme.md +139 -4
- package/docs/widget/Accordion.md +74 -8
- package/docs/widget/AdMob.md +68 -3
- package/docs/widget/AdSense.md +67 -2
- package/docs/widget/AlertDialog.md +1 -1
- package/docs/widget/AnimatedBox.md +80 -39
- package/docs/widget/AnimatedText.md +74 -18
- package/docs/widget/Audio.md +73 -14
- package/docs/widget/Avatar.md +69 -4
- package/docs/widget/Badge.md +68 -3
- package/docs/widget/BottomSheet.md +2 -2
- package/docs/widget/Button.md +69 -3
- package/docs/widget/Card.md +69 -4
- package/docs/widget/Carousel.md +67 -2
- package/docs/widget/Chart.md +67 -2
- package/docs/widget/Checkbox.md +74 -8
- package/docs/widget/Chip.md +68 -3
- package/docs/widget/CircularBar.md +74 -15
- package/docs/widget/CircularChart.md +71 -10
- package/docs/widget/CodeViewer.md +67 -2
- package/docs/widget/Column.md +68 -3
- package/docs/widget/Container.md +68 -3
- package/docs/widget/DataTable.md +67 -2
- package/docs/widget/Divider.md +67 -2
- package/docs/widget/DraggBox.md +67 -2
- package/docs/widget/Dropdown.md +76 -10
- package/docs/widget/DroppBox.md +67 -2
- package/docs/widget/FloatingActionButton.md +68 -3
- package/docs/widget/GridView.md +67 -2
- package/docs/widget/Icon.md +71 -5
- package/docs/widget/Image.md +68 -3
- package/docs/widget/Input.md +68 -3
- package/docs/widget/Inspector.md +2 -2
- package/docs/widget/InstallButton.md +73 -13
- package/docs/widget/ListTile.md +69 -3
- package/docs/widget/ListView.md +67 -2
- package/docs/widget/Maps.md +68 -3
- package/docs/widget/Markdown.md +74 -15
- package/docs/widget/MatrixRain.md +72 -11
- package/docs/widget/Menu.md +209 -0
- package/docs/widget/Modal.md +3 -3
- package/docs/widget/Pagination.md +68 -3
- package/docs/widget/ParallaxBox.md +72 -12
- package/docs/widget/ProgressBar.md +69 -4
- package/docs/widget/QRCode.md +67 -2
- package/docs/widget/README.md +54 -50
- package/docs/widget/Radio.md +76 -10
- package/docs/widget/Rating.md +70 -6
- package/docs/widget/Row.md +68 -3
- package/docs/widget/Skeleton.md +68 -3
- package/docs/widget/Slider.md +75 -9
- package/docs/widget/SnackBar.md +2 -2
- package/docs/widget/Stack.md +68 -3
- package/docs/widget/Stepper.md +68 -3
- package/docs/widget/Switch.md +74 -8
- package/docs/widget/Text.md +71 -4
- package/docs/widget/ThemePicker.md +197 -0
- package/docs/widget/Tooltip.md +72 -7
- package/docs/widget/TreeView.md +70 -10
- package/docs/widget/Video.md +71 -10
- package/package.json +10 -3
- package/skills/fletbox-design/SKILL.md +89 -5
- package/skills/fletbox-motion/SKILL.md +1 -1
- package/skills/fletbox-rules/SKILL.md +56 -4
- package/src/core/runApp.js +12 -0
- package/src/index.d.ts +374 -101
- package/src/index.js +9 -0
- package/src/navigations/AdaptiveScaffold.js +2 -2
- package/src/navigations/BottomNavigation.js +1 -1
- package/src/navigations/Scaffold.js +18 -6
- package/src/tools/useState.js +11 -4
- package/src/utils/colors.js +275 -0
- package/src/utils/errorBoundary.js +288 -0
- package/src/utils/index.js +10 -2
- package/src/utils/stackPosition.js +39 -24
- package/src/utils/syntaxHighlight.js +4 -1
- package/src/utils/themes.js +279 -3
- package/src/widget-factory/addChildren.js +17 -10
- package/src/widget-factory/assignProps.js +18 -6
- package/src/widget-factory/createWidget.js +7 -0
- package/src/widget-factory/deprecations.js +77 -0
- package/src/widget-factory/translateProps.js +11 -1
- package/src/widget-factory/warnings.js +1 -27
- package/src/widget-factory/widgetFactory.js +59 -17
- package/src/widgets/Accordion.js +21 -7
- package/src/widgets/Badge.js +11 -2
- package/src/widgets/Button.js +15 -3
- package/src/widgets/Carousel.js +12 -4
- package/src/widgets/Chart.js +7 -2
- package/src/widgets/Checkbox.js +1 -2
- package/src/widgets/Chip.js +24 -10
- package/src/widgets/CircularBar.js +9 -3
- package/src/widgets/CircularChart.js +5 -3
- package/src/widgets/Container.js +6 -0
- package/src/widgets/Divider.js +3 -1
- package/src/widgets/DraggBox.js +9 -0
- package/src/widgets/Dropdown.js +3 -3
- package/src/widgets/DroppBox.js +5 -0
- package/src/widgets/FloatingActionButton.js +1 -1
- package/src/widgets/Icon.js +5 -1
- package/src/widgets/InstallButton.js +4 -2
- package/src/widgets/Markdown.js +171 -103
- package/src/widgets/Menu.js +340 -0
- package/src/widgets/Modal.js +7 -1
- package/src/widgets/Pagination.js +10 -5
- package/src/widgets/ProgressBar.js +5 -4
- package/src/widgets/QRCode.js +4 -0
- package/src/widgets/Radio.js +1 -2
- package/src/widgets/Rating.js +9 -0
- package/src/widgets/Slider.js +4 -1
- package/src/widgets/SnackBar.js +2 -2
- package/src/widgets/Switch.js +1 -2
- package/src/widgets/Text.js +9 -5
- package/src/widgets/ThemePicker.js +234 -0
- package/src/widgets/TreeView.js +2 -1
- package/src/widgets/index.js +2 -0
- package/docs/CONTRIBUTING.md +0 -156
package/src/utils/themes.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/components/flet-box/utils/themes.js
|
|
2
2
|
import { readableInk } from "./readableInk.js";
|
|
3
|
+
import { ACCENT_PRESETS, accentSurfaces, accentTokens, accentStop } from "./colors.js";
|
|
3
4
|
|
|
4
5
|
// ========== DEFAULT COLORS (static, safe) ==========
|
|
5
6
|
const DEFAULT_COLORS = {
|
|
@@ -325,9 +326,30 @@ const syncBrowserChrome = () => {
|
|
|
325
326
|
const applyPalette = (themeName) => {
|
|
326
327
|
const palette = palettes[themeName];
|
|
327
328
|
if (palette) {
|
|
328
|
-
|
|
329
|
+
// The accent sits on top of the neutral scale and owns three tokens by
|
|
330
|
+
// default — `primary`, the ink that reads on it and its tint — so switching
|
|
331
|
+
// theme keeps every surface and moves only the brand. Worn as a *theme*
|
|
332
|
+
// (`setAccent(id, { surfaces: true })`) it also writes the page, the panels,
|
|
333
|
+
// the cards and the hairlines off the same five stops, which is the difference
|
|
334
|
+
// between a colored button and a colored app. Merging here, rather than
|
|
335
|
+
// writing the tokens straight onto `colors`, is what makes the choice survive a
|
|
336
|
+
// `setTheme`, an OS flip and a re-register: the layer is re-applied on every
|
|
337
|
+
// repaint instead of being overwritten by the next palette.
|
|
338
|
+
//
|
|
339
|
+
// It therefore also wins over a palette's own `primary`, including one an app
|
|
340
|
+
// wrote with `registerPalette` — the reader's choice of color is the later
|
|
341
|
+
// word. An app that wants its brand back calls `setAccent(null)`.
|
|
342
|
+
const accent = activeAccent ? accents[activeAccent] : null;
|
|
343
|
+
const layer = accent
|
|
344
|
+
? {
|
|
345
|
+
...accent[themeName],
|
|
346
|
+
...(accentSurfacesOn && accent[`${themeName}Surfaces`]),
|
|
347
|
+
}
|
|
348
|
+
: null;
|
|
349
|
+
const merged = layer ? { ...palette, ...layer } : palette;
|
|
350
|
+
Object.keys(merged).forEach((key) => {
|
|
329
351
|
if (colors.hasOwnProperty(key)) {
|
|
330
|
-
colors[key] =
|
|
352
|
+
colors[key] = merged[key];
|
|
331
353
|
}
|
|
332
354
|
});
|
|
333
355
|
}
|
|
@@ -402,6 +424,243 @@ export const registerPalette = (themeName, palette) => {
|
|
|
402
424
|
return palettes[themeName];
|
|
403
425
|
};
|
|
404
426
|
|
|
427
|
+
// ========== ACCENTS ==========
|
|
428
|
+
/**
|
|
429
|
+
* The selectable brand colors, keyed by id.
|
|
430
|
+
*
|
|
431
|
+
* An accent is not a fourth palette: it is the three tokens a palette cannot
|
|
432
|
+
* agree on between its own two themes. `palettes.light` and `palettes.dark`
|
|
433
|
+
* still own every surface, border and ink; this map owns only `primary`,
|
|
434
|
+
* `onPrimary` and `primaryBg`, and `applyPalette` lays it over whichever palette
|
|
435
|
+
* is live. That is why one tap changes the buttons, icons and selection of the
|
|
436
|
+
* screen without changing whether the screen is dark.
|
|
437
|
+
*
|
|
438
|
+
* @type {Object<string, {id: string, label: string, light: Object, dark: Object}>}
|
|
439
|
+
*/
|
|
440
|
+
const accents = {};
|
|
441
|
+
|
|
442
|
+
/** What the stored value puts between a family's id and "it also paints the page". */
|
|
443
|
+
const SURFACES_SUFFIX = "+surfaces";
|
|
444
|
+
|
|
445
|
+
/** The accent currently laid over the palette, or `null` for the palette's own. */
|
|
446
|
+
let activeAccent = null;
|
|
447
|
+
|
|
448
|
+
/** Whether that accent also paints the page and its panels, or only the brand. */
|
|
449
|
+
let accentSurfacesOn = false;
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* The three tokens one end of an accent writes, from either shape an accent can
|
|
453
|
+
* arrive in: a family's ramp, whose wash is a designed stop, or a bare hex, whose
|
|
454
|
+
* wash has to be derived.
|
|
455
|
+
*
|
|
456
|
+
* @param {string|string[]} spec - The accent for one theme.
|
|
457
|
+
* @param {"light"|"dark"} theme
|
|
458
|
+
*/
|
|
459
|
+
const accentLayer = (spec, theme) =>
|
|
460
|
+
accentTokens(
|
|
461
|
+
Array.isArray(spec) ? accentStop(spec, theme) : spec,
|
|
462
|
+
theme,
|
|
463
|
+
Array.isArray(spec) ? spec : undefined
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* The page and panel tokens the same end writes when the accent is worn as a
|
|
468
|
+
* theme. Only a ramp can produce them: five stops are a scale, and a bare brand
|
|
469
|
+
* hex is one color with no lightness to walk.
|
|
470
|
+
*
|
|
471
|
+
* @param {string|string[]} spec
|
|
472
|
+
* @param {"light"|"dark"} theme
|
|
473
|
+
* @returns {Object|null}
|
|
474
|
+
*/
|
|
475
|
+
const accentSurfaceLayer = (spec, theme) =>
|
|
476
|
+
Array.isArray(spec) ? accentSurfaces(spec, theme) : null;
|
|
477
|
+
|
|
478
|
+
const putAccent = (preset) => {
|
|
479
|
+
const { id, label, light, dark } = preset || {};
|
|
480
|
+
if (!id || !light || !dark) return null;
|
|
481
|
+
accents[id] = {
|
|
482
|
+
id,
|
|
483
|
+
label: label || id,
|
|
484
|
+
light: accentLayer(light, "light"),
|
|
485
|
+
dark: accentLayer(dark, "dark"),
|
|
486
|
+
lightSurfaces: accentSurfaceLayer(light, "light"),
|
|
487
|
+
darkSurfaces: accentSurfaceLayer(dark, "dark"),
|
|
488
|
+
// Kept only when the accent arrived as a ramp; a picker paints the family,
|
|
489
|
+
// not just its accent stop, and a pair of hexes has no family to show.
|
|
490
|
+
ramp: Array.isArray(light) || Array.isArray(dark) ? { light, dark } : null,
|
|
491
|
+
};
|
|
492
|
+
return accents[id];
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
ACCENT_PRESETS.forEach(putAccent);
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Writes the reader's accent choice down, or removes it. `window` is the test, not
|
|
499
|
+
* a try/catch around the call, for the same reason `setTheme` uses it: Node 22+
|
|
500
|
+
* has a global `localStorage` that warns and throws.
|
|
501
|
+
*
|
|
502
|
+
* Surfaces ride on the end of the same value (`"teal+surfaces"`) so the stored
|
|
503
|
+
* form stays a string a person can read in devtools, and a bare id — what every
|
|
504
|
+
* app wrote before surfaces existed — still parses.
|
|
505
|
+
* @param {string|null} id
|
|
506
|
+
* @param {boolean} surfaces
|
|
507
|
+
*/
|
|
508
|
+
const persistAccent = (id, surfaces) => {
|
|
509
|
+
if (typeof window === "undefined") return;
|
|
510
|
+
try {
|
|
511
|
+
if (id) {
|
|
512
|
+
localStorage.setItem(
|
|
513
|
+
"fletbox-accent",
|
|
514
|
+
surfaces ? `${id}${SURFACES_SUFFIX}` : id,
|
|
515
|
+
);
|
|
516
|
+
} else localStorage.removeItem("fletbox-accent");
|
|
517
|
+
} catch (e) {
|
|
518
|
+
// A full or private store is not a reason to stop painting the page.
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* The stored accent, when it is one the engine knows. A stale id — a family an
|
|
524
|
+
* older build shipped and this one dropped — reads as "no choice" rather than
|
|
525
|
+
* painting a color nothing derives from.
|
|
526
|
+
* @returns {{id: string, surfaces: boolean}|null}
|
|
527
|
+
*/
|
|
528
|
+
const readStoredAccent = () => {
|
|
529
|
+
try {
|
|
530
|
+
const raw = localStorage.getItem("fletbox-accent");
|
|
531
|
+
if (!raw) return null;
|
|
532
|
+
const surfaces = raw.endsWith(SURFACES_SUFFIX);
|
|
533
|
+
const id = surfaces ? raw.slice(0, -SURFACES_SUFFIX.length) : raw;
|
|
534
|
+
return id && accents[id] ? { id, surfaces } : null;
|
|
535
|
+
} catch (e) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Adds (or replaces) one selectable accent.
|
|
542
|
+
*
|
|
543
|
+
* The pair is what a picker shows and what the engine paints: `light` is the deep
|
|
544
|
+
* end of the hue, dark enough to carry white words on a light page, and `dark` is
|
|
545
|
+
* the lifted end, bright enough to carry a dark page's own ink. The ink and the
|
|
546
|
+
* soft tint are derived from each stop, so an app cannot register a family whose
|
|
547
|
+
* label is unreadable — the same derivation the shipped presets pass.
|
|
548
|
+
*
|
|
549
|
+
* Each end may be a single hex or a five-stop ramp (dark-to-light in `light`,
|
|
550
|
+
* bright-to-dark in `dark`), in which case the accent and its wash are read off
|
|
551
|
+
* the stops the shipped families use.
|
|
552
|
+
*
|
|
553
|
+
* Repaints only when the id being replaced is the one on screen.
|
|
554
|
+
*
|
|
555
|
+
* @param {Object} preset
|
|
556
|
+
* @param {string} preset.id - Stable key, e.g. `"teal"`.
|
|
557
|
+
* @param {string} [preset.label] - Accessible name shown to the reader; defaults to `id`.
|
|
558
|
+
* @param {string|string[]} preset.light - The accent, or its ramp, for the light theme.
|
|
559
|
+
* @param {string|string[]} preset.dark - The accent, or its ramp, for the dark theme.
|
|
560
|
+
* @returns {Object|null} The stored accent, or `null` when the preset is incomplete.
|
|
561
|
+
*/
|
|
562
|
+
export const registerAccent = (preset) => {
|
|
563
|
+
const accent = putAccent(preset);
|
|
564
|
+
if (!accent) return null;
|
|
565
|
+
if (activeAccent === accent.id) applyPalette(getTheme());
|
|
566
|
+
return accent;
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Lays an accent over the live palette and persists the choice.
|
|
571
|
+
*
|
|
572
|
+
* Pass `null` (or an unknown id) to take it back off: the screen returns to
|
|
573
|
+
* whatever `primary` the palette itself ships, and the stored preference is
|
|
574
|
+
* cleared so the next boot does it too.
|
|
575
|
+
*
|
|
576
|
+
* With `{ surfaces: true }` the family is worn as a whole theme: the same five
|
|
577
|
+
* stops also write `background`, `surface`, `card`, `border` and `divider`, so the
|
|
578
|
+
* page, the panels and the hairlines take the hue and not only the buttons. The
|
|
579
|
+
* ink scale, the focus ring and the semantic signals stay the palette's — a tinted
|
|
580
|
+
* theme still has to be able to say "this failed". An accent registered as a bare
|
|
581
|
+
* pair of hexes has no scale to walk, so it keeps painting brand only.
|
|
582
|
+
*
|
|
583
|
+
* @param {string|null} id - A registered accent, or `null` for the palette's own.
|
|
584
|
+
* @param {Object} [options]
|
|
585
|
+
* @param {boolean} [options.surfaces] - Also paint the page and its panels.
|
|
586
|
+
* @returns {Object} The updated `colors` object.
|
|
587
|
+
*/
|
|
588
|
+
export const setAccent = (id, options) => {
|
|
589
|
+
if (id && !accents[id]) return colors;
|
|
590
|
+
activeAccent = id || null;
|
|
591
|
+
accentSurfacesOn = Boolean(
|
|
592
|
+
activeAccent && options?.surfaces && accents[activeAccent].lightSurfaces,
|
|
593
|
+
);
|
|
594
|
+
persistAccent(activeAccent, accentSurfacesOn);
|
|
595
|
+
applyPalette(getTheme());
|
|
596
|
+
return colors;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Applies an accent only when the reader has never picked one.
|
|
601
|
+
*
|
|
602
|
+
* This is the one an app calls at boot with its own brand: it sets the default
|
|
603
|
+
* without writing the reader's preference down, so a picker still shows nothing
|
|
604
|
+
* selected on the first visit and a later choice still wins over it.
|
|
605
|
+
*
|
|
606
|
+
* @param {string} id - A registered accent.
|
|
607
|
+
* @param {Object} [options]
|
|
608
|
+
* @param {boolean} [options.surfaces] - Wear it as a theme, not only as a brand.
|
|
609
|
+
* @returns {string|null} The accent now active.
|
|
610
|
+
*/
|
|
611
|
+
export const applyAccentPreference = (id, options) => {
|
|
612
|
+
if (readStoredAccent() || !accents[id]) return activeAccent;
|
|
613
|
+
activeAccent = id;
|
|
614
|
+
accentSurfacesOn = Boolean(
|
|
615
|
+
options?.surfaces && accents[id].lightSurfaces,
|
|
616
|
+
);
|
|
617
|
+
applyPalette(getTheme());
|
|
618
|
+
return activeAccent;
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* The accent currently laid over the palette.
|
|
623
|
+
* @returns {string|null} Its id, or `null` when the palette's own `primary` is showing.
|
|
624
|
+
*/
|
|
625
|
+
export const getAccent = () => activeAccent;
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Whether the active accent also paints the page and its panels, or only the brand.
|
|
629
|
+
*
|
|
630
|
+
* A picker needs this to decide whether a tap on the cell already ticked is a
|
|
631
|
+
* no-op: the same family means two different things, and re-tapping it to move
|
|
632
|
+
* between them must repaint.
|
|
633
|
+
*
|
|
634
|
+
* @returns {boolean}
|
|
635
|
+
*/
|
|
636
|
+
export const getAccentSurfaces = () => accentSurfacesOn;
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Every selectable accent, in registration order, as the two stops a picker paints.
|
|
640
|
+
* @returns {Array<{id: string, label: string, light: string, dark: string}>}
|
|
641
|
+
*/
|
|
642
|
+
export const listAccents = () =>
|
|
643
|
+
Object.values(accents).map(({ id, label, light, dark }) => ({
|
|
644
|
+
id,
|
|
645
|
+
label,
|
|
646
|
+
light: light.primary,
|
|
647
|
+
dark: dark.primary,
|
|
648
|
+
}));
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* The same list with each family's five stops beside it, for a picker that shows
|
|
652
|
+
* the ramp rather than one square: `light` runs dark-to-light and `dark`
|
|
653
|
+
* bright-to-dark, so the accent stop of each theme is the one `listAccents` names.
|
|
654
|
+
* @returns {Array<{id: string, label: string, light: string[], dark: string[]}>}
|
|
655
|
+
*/
|
|
656
|
+
export const listAccentRamps = () =>
|
|
657
|
+
Object.values(accents).filter(({ ramp }) => ramp).map(({ id, label, ramp }) => ({
|
|
658
|
+
id,
|
|
659
|
+
label,
|
|
660
|
+
light: ramp.light,
|
|
661
|
+
dark: ramp.dark,
|
|
662
|
+
}));
|
|
663
|
+
|
|
405
664
|
/**
|
|
406
665
|
* Returns the currently active theme name.
|
|
407
666
|
* Reads from `localStorage` first; falls back to the OS
|
|
@@ -528,7 +787,10 @@ export const ThemeProvider = ({ children, theme = null }) => {
|
|
|
528
787
|
* getTheme: function,
|
|
529
788
|
* toggleTheme: function,
|
|
530
789
|
* getColor: function,
|
|
531
|
-
* applySystemTheme: function
|
|
790
|
+
* applySystemTheme: function,
|
|
791
|
+
* setAccent: function,
|
|
792
|
+
* getAccent: function,
|
|
793
|
+
* listAccents: function
|
|
532
794
|
* }}
|
|
533
795
|
*/
|
|
534
796
|
export const useTheme = () => {
|
|
@@ -539,12 +801,21 @@ export const useTheme = () => {
|
|
|
539
801
|
toggleTheme,
|
|
540
802
|
getColor,
|
|
541
803
|
applySystemTheme,
|
|
804
|
+
setAccent,
|
|
805
|
+
getAccent,
|
|
806
|
+
listAccents,
|
|
542
807
|
};
|
|
543
808
|
};
|
|
544
809
|
|
|
545
810
|
// ========== INITIALIZATION (safe) ==========
|
|
546
811
|
const initTheme = () => {
|
|
547
812
|
try {
|
|
813
|
+
// Restored before the first paint, not after: laying the accent on afterwards
|
|
814
|
+
// would repaint the whole page a frame into boot and flash the brand color
|
|
815
|
+
// over the reader's own choice.
|
|
816
|
+
const stored = readStoredAccent();
|
|
817
|
+
activeAccent = stored?.id ?? null;
|
|
818
|
+
accentSurfacesOn = Boolean(stored?.surfaces);
|
|
548
819
|
const savedTheme = localStorage.getItem("fletbox-theme");
|
|
549
820
|
if (savedTheme && (savedTheme === "light" || savedTheme === "dark")) {
|
|
550
821
|
applyPalette(savedTheme);
|
|
@@ -580,6 +851,11 @@ export default {
|
|
|
580
851
|
subscribeTheme,
|
|
581
852
|
applySystemTheme,
|
|
582
853
|
watchSystemTheme,
|
|
854
|
+
registerAccent,
|
|
855
|
+
setAccent,
|
|
856
|
+
applyAccentPreference,
|
|
857
|
+
getAccent,
|
|
858
|
+
listAccents,
|
|
583
859
|
ThemeProvider,
|
|
584
860
|
useTheme,
|
|
585
861
|
};
|
|
@@ -54,8 +54,13 @@ export const addChildren = (parent, children, options = {}) => {
|
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
* Removes
|
|
58
|
-
* `PROP_CHILDREN`).
|
|
57
|
+
* Removes the children that were previously appended via props (i.e. tracked in
|
|
58
|
+
* `PROP_CHILDREN`) and **returns them**, so a caller can put them back.
|
|
59
|
+
*
|
|
60
|
+
* The return value is the point: an update that swaps the content takes the old
|
|
61
|
+
* content out first, so if attaching the new content throws, the widget would
|
|
62
|
+
* be left empty — a hole where the content used to be. Keeping the nodes lets
|
|
63
|
+
* the caller restore them.
|
|
59
64
|
*
|
|
60
65
|
* Handles two cases:
|
|
61
66
|
* 1. **Normal path** – the node is still a direct child of `parent`. The
|
|
@@ -69,14 +74,14 @@ export const addChildren = (parent, children, options = {}) => {
|
|
|
69
74
|
*
|
|
70
75
|
* @param {HTMLElement} parent - The parent widget whose prop-children should be
|
|
71
76
|
* removed.
|
|
72
|
-
* @returns {
|
|
77
|
+
* @returns {Array<Node>} The nodes that were taken out.
|
|
73
78
|
*/
|
|
74
|
-
export const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
)
|
|
79
|
+
export const takePropChildren = (parent) => {
|
|
80
|
+
const taken = [];
|
|
81
|
+
|
|
82
|
+
Array.from(parent.childNodes).forEach((child) => {
|
|
83
|
+
if (!PROP_CHILDREN.has(child)) return;
|
|
78
84
|
|
|
79
|
-
toRemove.forEach((child) => {
|
|
80
85
|
if (child.parentNode === parent) {
|
|
81
86
|
// Normal path: removeChild override also cleans up _children.
|
|
82
87
|
parent.removeChild(child);
|
|
@@ -87,11 +92,13 @@ export const removePropChildren = (parent) => {
|
|
|
87
92
|
if (child._isWidget) child.parent = null;
|
|
88
93
|
}
|
|
89
94
|
PROP_CHILDREN.delete(child);
|
|
95
|
+
taken.push(child);
|
|
90
96
|
});
|
|
91
97
|
|
|
92
|
-
return
|
|
98
|
+
return taken;
|
|
93
99
|
};
|
|
94
100
|
|
|
101
|
+
|
|
95
102
|
/**
|
|
96
103
|
* Removes ALL children from a parent widget, regardless of whether they were
|
|
97
104
|
* appended via props or programmatically.
|
|
@@ -111,4 +118,4 @@ export const clearChildren = (parent) => {
|
|
|
111
118
|
return parent;
|
|
112
119
|
};
|
|
113
120
|
|
|
114
|
-
export default { addChildren,
|
|
121
|
+
export default { addChildren, takePropChildren, clearChildren };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// core/assignProps.js
|
|
2
2
|
import { setStyles } from "./tools.js";
|
|
3
3
|
import { resolveReactive } from "./reactiveValue.js";
|
|
4
|
+
import { guardHandler } from "../utils/errorBoundary.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Extracts the real value from a prop that may be a reactive accessor or a
|
|
@@ -154,11 +155,18 @@ export const assignProps = (widget, props) => {
|
|
|
154
155
|
if (props.events && Object.keys(props.events).length > 0) {
|
|
155
156
|
Object.entries(props.events).forEach(([event, handler]) => {
|
|
156
157
|
if (typeof handler !== "function") return;
|
|
158
|
+
// The handler is wrapped where it is attached, so a throw inside it is
|
|
159
|
+
// reported with the name of the widget that owns it instead of
|
|
160
|
+
// arriving as an anonymous stack.
|
|
161
|
+
const guarded = guardHandler(handler, {
|
|
162
|
+
widgetName: widget._widgetName,
|
|
163
|
+
event,
|
|
164
|
+
});
|
|
157
165
|
if (event === "click") {
|
|
158
|
-
widget.onclick =
|
|
166
|
+
widget.onclick = guarded;
|
|
159
167
|
} else {
|
|
160
|
-
widget.addEventListener(event,
|
|
161
|
-
widget._events.push({ event, handler });
|
|
168
|
+
widget.addEventListener(event, guarded);
|
|
169
|
+
widget._events.push({ event, handler: guarded });
|
|
162
170
|
}
|
|
163
171
|
});
|
|
164
172
|
}
|
|
@@ -219,11 +227,15 @@ export const assignProps = (widget, props) => {
|
|
|
219
227
|
|
|
220
228
|
if (key.startsWith("on") && typeof value === "function") {
|
|
221
229
|
const eventName = key.slice(2).toLowerCase();
|
|
230
|
+
const guarded = guardHandler(value, {
|
|
231
|
+
widgetName: widget._widgetName,
|
|
232
|
+
event: eventName,
|
|
233
|
+
});
|
|
222
234
|
if (eventName === "click") {
|
|
223
|
-
widget.onclick =
|
|
235
|
+
widget.onclick = guarded;
|
|
224
236
|
} else {
|
|
225
|
-
widget.addEventListener(eventName,
|
|
226
|
-
widget._events.push({ event: eventName, handler:
|
|
237
|
+
widget.addEventListener(eventName, guarded);
|
|
238
|
+
widget._events.push({ event: eventName, handler: guarded });
|
|
227
239
|
}
|
|
228
240
|
} else if (
|
|
229
241
|
[
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { ensureFocusRing } from "../utils/focusRing.js";
|
|
4
4
|
import { ensureFieldFont } from "../utils/fieldFont.js";
|
|
5
|
+
import { installErrorBoundary } from "../utils/errorBoundary.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Creates a bare DOM element for use as a widget.
|
|
@@ -19,6 +20,11 @@ import { ensureFieldFont } from "../utils/fieldFont.js";
|
|
|
19
20
|
* rule about text fields, and this is the only step that runs exactly once per
|
|
20
21
|
* widget without being owned by any of them.
|
|
21
22
|
*
|
|
23
|
+
* So does the error boundary: the global handler that catches the errors no
|
|
24
|
+
* widget owns (a rejected promise, a timer that exploded) belongs to the
|
|
25
|
+
* document, not to any single widget, and this is the one step every widget
|
|
26
|
+
* passes through.
|
|
27
|
+
*
|
|
22
28
|
* @param {string} tag - HTML tag name for the widget.
|
|
23
29
|
* Defaults to `'div'` when the value is not a string.
|
|
24
30
|
* @returns {HTMLElement} The newly created DOM element with `_events` initialised.
|
|
@@ -26,6 +32,7 @@ import { ensureFieldFont } from "../utils/fieldFont.js";
|
|
|
26
32
|
export const createWidget = (tag) => {
|
|
27
33
|
ensureFocusRing();
|
|
28
34
|
ensureFieldFont();
|
|
35
|
+
installErrorBoundary();
|
|
29
36
|
|
|
30
37
|
// Validateste that tag is a string; fall back to 'div' when it is not.
|
|
31
38
|
let finalTag = tag;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// The names FletBox still honours but no longer recommends.
|
|
2
|
+
//
|
|
3
|
+
// Every row is a promise to an app written against the old spelling: the prop
|
|
4
|
+
// keeps working, and the migration guide says what to write instead. This
|
|
5
|
+
// table is the single source — the guide (`docs/guides/migration.md`), the
|
|
6
|
+
// codemod in `audit-fix` and the contract test (`scripts/check-migrations.mjs`)
|
|
7
|
+
// all read it, so a rename cannot be documented in one place and forgotten in
|
|
8
|
+
// another.
|
|
9
|
+
//
|
|
10
|
+
// A row names a widget, because the same spelling means different things in
|
|
11
|
+
// different widgets: `onToggle` is the old name of `onPress` on `Switch` and
|
|
12
|
+
// on `Accordion`, and the *current* name on `TreeView`, so a flat list of
|
|
13
|
+
// names would send a `TreeView` to rename a prop that is already right.
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {object} Deprecation
|
|
17
|
+
* @property {string} widget The widget whose prop is the old spelling.
|
|
18
|
+
* @property {string} from The old prop name.
|
|
19
|
+
* @property {string|null} to The prop to write instead, or `null` when the old
|
|
20
|
+
* prop is read by nobody and the key is simply removed.
|
|
21
|
+
* @property {string} since The release that renamed it.
|
|
22
|
+
* @property {{ prop: string, value: string, to: string }} [alternate]
|
|
23
|
+
* The other target, when the call itself says which one applies.
|
|
24
|
+
* @property {string[]} [guard]
|
|
25
|
+
* Props whose presence means the old prop is already inert, so the codemod
|
|
26
|
+
* leaves the call alone instead of changing what the screen shows.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** @type {Deprecation[]} */
|
|
30
|
+
export const DEPRECATED_PROPS = [
|
|
31
|
+
{ widget: "Accordion", from: "onToggle", to: "onPress", since: "1.2.0" },
|
|
32
|
+
{ widget: "Checkbox", from: "onCheck", to: "onPress", since: "1.2.0" },
|
|
33
|
+
{ widget: "Dropdown", from: "onChange", to: "onPress", since: "1.2.0" },
|
|
34
|
+
{ widget: "Radio", from: "onSelect", to: "onPress", since: "1.2.0" },
|
|
35
|
+
{ widget: "Slider", from: "onChanged", to: "onPress", since: "1.2.0" },
|
|
36
|
+
{ widget: "Switch", from: "onToggle", to: "onPress", since: "1.2.0" },
|
|
37
|
+
{ widget: "Tooltip", from: "tooltipDelay", to: "delay", since: "1.2.0" },
|
|
38
|
+
{
|
|
39
|
+
widget: "Scaffold",
|
|
40
|
+
from: "navSideBar",
|
|
41
|
+
to: "leftNavBar",
|
|
42
|
+
since: "1.2.0",
|
|
43
|
+
// The legacy prop could sit on either side, and the legacy position prop
|
|
44
|
+
// is what said which. The codemod follows it instead of guessing a side.
|
|
45
|
+
alternate: { prop: "navSideBarPosition", value: "right", to: "rightNavBar" },
|
|
46
|
+
// `Scaffold` ignores the legacy sidebar once a new one is present, so a
|
|
47
|
+
// call that already has one must be left exactly as it is.
|
|
48
|
+
guard: ["leftNavBar", "rightNavBar"],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
widget: "Scaffold",
|
|
52
|
+
from: "navSideBarWidth",
|
|
53
|
+
to: "leftNavBarWidth",
|
|
54
|
+
since: "1.2.0",
|
|
55
|
+
alternate: { prop: "navSideBarPosition", value: "right", to: "rightNavBarWidth" },
|
|
56
|
+
guard: ["leftNavBarWidth", "rightNavBarWidth"],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
widget: "Scaffold",
|
|
60
|
+
from: "navSideBarPosition",
|
|
61
|
+
to: null,
|
|
62
|
+
since: "1.2.0",
|
|
63
|
+
// The position only ever said which side the *legacy* sidebar was on. Once
|
|
64
|
+
// the sidebar itself carries the side in its name, this prop is read by
|
|
65
|
+
// nobody and the key goes with it.
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The deprecation of one prop on one widget, or null.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} widget
|
|
73
|
+
* @param {string} prop
|
|
74
|
+
* @returns {Deprecation|null}
|
|
75
|
+
*/
|
|
76
|
+
export const deprecationFor = (widget, prop) =>
|
|
77
|
+
DEPRECATED_PROPS.find((entry) => entry.widget === widget && entry.from === prop) || null;
|
|
@@ -128,6 +128,11 @@ export const ALL_PROPS = [
|
|
|
128
128
|
{ prop: "wordBreak", domProp: "wordBreak", type: "style", unit: "none", reactive: true },
|
|
129
129
|
{ prop: "fontFamily", domProp: "fontFamily", type: "style", unit: "none", reactive: true },
|
|
130
130
|
{ prop: "textOverflow", domProp: "textOverflow", type: "style", unit: "none", reactive: true },
|
|
131
|
+
// Contorno de glifo: `textStroke: "4px #fff"` pinta un aro alrededor de las letras
|
|
132
|
+
// (-webkit-text-stroke) and `paintOrder: "stroke fill"` put it behind the fill,
|
|
133
|
+
// so the center keeps its own color instead of having the stroke absorb it.
|
|
134
|
+
{ prop: "textStroke", domProp: "webkitTextStroke", type: "style", unit: "none", reactive: true },
|
|
135
|
+
{ prop: "paintOrder", domProp: "paintOrder", type: "style", unit: "none", reactive: true },
|
|
131
136
|
|
|
132
137
|
// ============================================================
|
|
133
138
|
// FLEXBOX
|
|
@@ -167,7 +172,7 @@ export const ALL_PROPS = [
|
|
|
167
172
|
{ prop: "roundedLeft", domProp: "borderTopLeftRadius", type: "style", unit: "rem", reactive: true, targets: ["borderTopLeftRadius", "borderBottomLeftRadius"] },
|
|
168
173
|
{ prop: "roundedRight", domProp: "borderTopRightRadius", type: "style", unit: "rem", reactive: true, targets: ["borderTopRightRadius", "borderBottomRightRadius"] },
|
|
169
174
|
|
|
170
|
-
//
|
|
175
|
+
// Symmetric aliases (used in only_controls: Image({ borderRadiusTop: 20 }))
|
|
171
176
|
{ prop: "borderRadiusTop", domProp: "borderTopLeftRadius", type: "style", unit: "rem", reactive: true, targets: ["borderTopLeftRadius", "borderTopRightRadius"] },
|
|
172
177
|
{ prop: "borderRadiusBottom", domProp: "borderBottomLeftRadius", type: "style", unit: "rem", reactive: true, targets: ["borderBottomLeftRadius", "borderBottomRightRadius"] },
|
|
173
178
|
{ prop: "borderRadiusLeft", domProp: "borderTopLeftRadius", type: "style", unit: "rem", reactive: true, targets: ["borderTopLeftRadius", "borderBottomLeftRadius"] },
|
|
@@ -502,6 +507,11 @@ export const ALL_PROPS = [
|
|
|
502
507
|
{ prop: "unselectedColor", domProp: "unselectedColor", type: "special" },
|
|
503
508
|
{ prop: "showLabels", domProp: "showLabels", type: "special" },
|
|
504
509
|
|
|
510
|
+
// ---- ThemePicker ----
|
|
511
|
+
// The families a picker offers. `listAccents()` is the default and the widget
|
|
512
|
+
// reads the list to build one swatch per entry, so it must never reach the DOM.
|
|
513
|
+
{ prop: "accents", domProp: "accents", type: "special" },
|
|
514
|
+
|
|
505
515
|
// ---- DrawerItem / CollapsibleSideBar ----
|
|
506
516
|
{ prop: "route", domProp: "route", type: "special" },
|
|
507
517
|
{ prop: "trailingIcon", domProp: "trailingIcon", type: "special" },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// core/warnings.js
|
|
2
2
|
// FletBox Warning System – Elegant, minimal, helpful.
|
|
3
3
|
|
|
4
|
-
// 🚀
|
|
4
|
+
// 🚀 Environment gate: disable warnings in production
|
|
5
5
|
const IS_DEV = typeof FLETBOX_DEV !== 'undefined' ? FLETBOX_DEV : true;
|
|
6
6
|
|
|
7
7
|
const suggestionCache = new Map();
|
|
@@ -13,7 +13,6 @@ let propNames = [];
|
|
|
13
13
|
// the cost of a known prop. Because `update()` re-applies every original prop, one
|
|
14
14
|
// unknown prop slowed that widget for its whole lifetime.
|
|
15
15
|
const warnedProps = new Set();
|
|
16
|
-
const warnedDeprecated = new Set();
|
|
17
16
|
|
|
18
17
|
export function setPropNames(names) {
|
|
19
18
|
propNames = names;
|
|
@@ -240,30 +239,6 @@ export function warnUnknownProp(propName, context = {}) {
|
|
|
240
239
|
console.groupEnd();
|
|
241
240
|
}
|
|
242
241
|
|
|
243
|
-
export function warnDeprecatedProp(propName, replacement = null, context = {}) {
|
|
244
|
-
if (!IS_DEV) return;
|
|
245
|
-
// Same reasoning as `warnUnknownProp`: one report per prop/replacement pair.
|
|
246
|
-
const deprecatedKey = `${propName}\u0000${replacement || ""}`;
|
|
247
|
-
if (warnedDeprecated.has(deprecatedKey)) return;
|
|
248
|
-
warnedDeprecated.add(deprecatedKey);
|
|
249
|
-
|
|
250
|
-
const location = getCallLocation();
|
|
251
|
-
|
|
252
|
-
printHeader("DEPRECATED PROP");
|
|
253
|
-
printProp(propName, "is deprecated");
|
|
254
|
-
if (replacement) {
|
|
255
|
-
console.log(
|
|
256
|
-
` %c🔄 Use: %c"${replacement}"%c instead`,
|
|
257
|
-
"color: #94a3b8; font-size: 13px;",
|
|
258
|
-
"color: #34d399; font-weight: 700; font-size: 16px;",
|
|
259
|
-
"color: #94a3b8; font-size: 13px;",
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
printLocation(location);
|
|
263
|
-
printWidget(context.widgetName);
|
|
264
|
-
console.groupEnd();
|
|
265
|
-
}
|
|
266
|
-
|
|
267
242
|
export function warnInvalidPropType(propName, expected, actual, context = {}) {
|
|
268
243
|
if (!IS_DEV) return;
|
|
269
244
|
|
|
@@ -292,7 +267,6 @@ export function clearSuggestionCache() {
|
|
|
292
267
|
|
|
293
268
|
export default {
|
|
294
269
|
warnUnknownProp,
|
|
295
|
-
warnDeprecatedProp,
|
|
296
270
|
warnInvalidPropType,
|
|
297
271
|
clearSuggestionCache,
|
|
298
272
|
setPropNames,
|