@blukavantui/community 1.1.0 → 1.2.0
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/dist/build-audit.json +127 -65
- package/dist/checkbox.js +1 -1
- package/dist/chunk-4LHIQROI.js +0 -0
- package/dist/chunk-6T677NSC.js +1 -0
- package/dist/chunk-7CEOCZ5L.js +0 -0
- package/dist/chunk-7DXFNK7C.js +1 -0
- package/dist/chunk-CNIIMGT3.js +0 -0
- package/dist/chunk-CSTCEBEK.js +1 -0
- package/dist/chunk-DRZM2AL3.js +0 -0
- package/dist/{chunk-JVE6CXCJ.js → chunk-EPPETNK6.js} +1 -1
- package/dist/chunk-EPPSBHZR.js +1 -0
- package/dist/{chunk-NTFHNL7T.js → chunk-G6OW4OZM.js} +1 -1
- package/dist/chunk-GS67ZBLI.js +1 -0
- package/dist/{chunk-6EHKOVZK.js → chunk-HVR2UDFX.js} +1 -1
- package/dist/chunk-IKYBFMHI.js +1 -0
- package/dist/chunk-KVOA55NE.js +0 -0
- package/dist/{chunk-MZBB5ROJ.js → chunk-LF2HELEZ.js} +1 -1
- package/dist/{chunk-UEOCR2C3.js → chunk-OLDA73XG.js} +2 -2
- package/dist/{chunk-IZJN3EF7.js → chunk-T45FJNEQ.js} +1 -1
- package/dist/chunk-TZSX4OTV.js +1 -0
- package/dist/{chunk-GK3INPAZ.js → chunk-V2YPIC6Z.js} +1 -1
- package/dist/chunk-V4ZQLQ7L.js +0 -0
- package/dist/chunk-VAM3FPCS.js +0 -0
- package/dist/chunk-VSTKXZYY.js +0 -0
- package/dist/chunk-WG4US4JH.js +1 -0
- package/dist/chunk-WGSTNXAQ.js +0 -0
- package/dist/chunk-WJHCVECU.js +1 -0
- package/dist/chunk-Y4AXWTAD.js +0 -0
- package/dist/chunk-YLFXSHKZ.js +0 -0
- package/dist/chunk-ZDGZGAYG.js +0 -0
- package/dist/command-palette.js +1 -1
- package/dist/component-motion.css +39 -0
- package/dist/component-motion.d.ts +45 -0
- package/dist/component-motion.js +1 -0
- package/dist/content.js +1 -1
- package/dist/core.css +8 -0
- package/dist/core.js +1 -1
- package/dist/data-motion.css +4 -0
- package/dist/data-motion.d.ts +23 -0
- package/dist/data-motion.js +1 -0
- package/dist/data.js +1 -1
- package/dist/date-picker.js +1 -1
- package/dist/date-range-picker.js +1 -1
- package/dist/date-time-picker.js +1 -1
- package/dist/drawer.js +1 -1
- package/dist/essentials.css +8 -0
- package/dist/feedback.js +1 -1
- package/dist/form-layout.js +1 -1
- package/dist/index.js +1 -1
- package/dist/inputs.js +1 -1
- package/dist/layout.js +1 -1
- package/dist/modal.js +1 -1
- package/dist/motion/action-feedback.d.ts +14 -0
- package/dist/motion/border-glow.d.ts +12 -0
- package/dist/motion/button.d.ts +11 -0
- package/dist/motion/component-settings.d.ts +51 -0
- package/dist/motion/controller.d.ts +33 -0
- package/dist/motion/effects.d.ts +30 -0
- package/dist/motion/feedback.d.ts +6 -0
- package/dist/motion/loading.d.ts +9 -0
- package/dist/motion/optional.d.ts +35 -0
- package/dist/motion/photo.d.ts +7 -0
- package/dist/motion/presence.d.ts +12 -0
- package/dist/motion/scene.d.ts +10 -0
- package/dist/motion/screen.d.ts +6 -0
- package/dist/motion/scroll-reveal.d.ts +13 -0
- package/dist/motion/settings-group.d.ts +14 -0
- package/dist/motion/stagger.d.ts +12 -0
- package/dist/motion/structure.d.ts +18 -0
- package/dist/motion/tabs.d.ts +19 -0
- package/dist/motion/tokens.d.ts +11 -0
- package/dist/motion/value.d.ts +21 -0
- package/dist/motion/view-transition.d.ts +14 -0
- package/dist/motion-enhancers.d.ts +12 -0
- package/dist/motion-enhancers.js +1 -0
- package/dist/motion-settings.d.ts +3 -0
- package/dist/motion-settings.js +1 -0
- package/dist/motion.css +1076 -0
- package/dist/motion.d.ts +20 -0
- package/dist/motion.js +1 -0
- package/dist/native/form-layout.js +1 -1
- package/dist/native-motion-components.d.ts +3 -0
- package/dist/native-motion-content.d.ts +1 -0
- package/dist/native-motion-foundations.d.ts +3 -0
- package/dist/native-motion-layout.d.ts +3 -0
- package/dist/native-motion-pilot.d.ts +34 -0
- package/dist/native-motion.d.ts +71 -0
- package/dist/notification.js +1 -1
- package/dist/pagination.js +1 -1
- package/dist/radio-button.js +1 -1
- package/dist/radio-group.js +1 -1
- package/dist/sidebar.js +1 -1
- package/dist/sizes.json +273 -151
- package/dist/skeleton.css +8 -0
- package/dist/styles.css +1130 -0
- package/dist/switch.js +1 -1
- package/dist/table.js +1 -1
- package/dist/time-picker.js +1 -1
- package/dist/tree-view.js +1 -1
- package/dist/web-motion/chip-ripple.d.ts +6 -0
- package/dist/web-motion/choice-ripple.d.ts +6 -0
- package/dist/web-motion/content.d.ts +9 -0
- package/dist/web-motion/data-rows.d.ts +12 -0
- package/dist/web-motion/fields.d.ts +6 -0
- package/dist/web-motion/navigation.d.ts +6 -0
- package/dist/web-motion/notifications.d.ts +6 -0
- package/dist/web-motion/select-ripple.d.ts +6 -0
- package/dist/web-motion/statistic.d.ts +13 -0
- package/dist/web-motion/surfaces.d.ts +10 -0
- package/dist/web-motion/tree.d.ts +6 -0
- package/docs/FORM-LAYOUT-ESSENTIALS.md +4 -4
- package/edition.json +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +29 -3
- package/scripts/build-motion-css.mjs +36 -0
- package/scripts/build-motion-styles.mjs +29 -0
- package/scripts/build.mjs +5 -1
- package/scripts/motion-entries.mjs +2 -0
- package/src/component-motion.ts +101 -0
- package/src/data-motion.ts +60 -0
- package/src/form-layout/choice.ts +17 -1
- package/src/motion/action-feedback.ts +38 -0
- package/src/motion/border-glow.ts +41 -0
- package/src/motion/button.ts +56 -0
- package/src/motion/component-settings.ts +147 -0
- package/src/motion/controller.ts +191 -0
- package/src/motion/effects.ts +75 -0
- package/src/motion/feedback.ts +41 -0
- package/src/motion/loading.ts +40 -0
- package/src/motion/optional.ts +123 -0
- package/src/motion/photo.ts +19 -0
- package/src/motion/presence.ts +44 -0
- package/src/motion/scene.ts +103 -0
- package/src/motion/screen.ts +15 -0
- package/src/motion/scroll-reveal.ts +49 -0
- package/src/motion/settings-group.ts +28 -0
- package/src/motion/stagger.ts +26 -0
- package/src/motion/structure.ts +75 -0
- package/src/motion/tabs.ts +105 -0
- package/src/motion/tokens.ts +2 -0
- package/src/motion/value.ts +61 -0
- package/src/motion/view-transition.ts +57 -0
- package/src/motion-enhancers.ts +12 -0
- package/src/motion-settings.ts +3 -0
- package/src/motion.ts +20 -0
- package/src/native-motion-components.ts +76 -0
- package/src/native-motion-content.ts +10 -0
- package/src/native-motion-foundations.ts +56 -0
- package/src/native-motion-layout.ts +35 -0
- package/src/native-motion-pilot.ts +151 -0
- package/src/native-motion.ts +188 -0
- package/src/web-motion/chip-ripple.ts +31 -0
- package/src/web-motion/choice-ripple.ts +30 -0
- package/src/web-motion/content.ts +74 -0
- package/src/web-motion/data-rows.ts +106 -0
- package/src/web-motion/fields.ts +52 -0
- package/src/web-motion/navigation.ts +62 -0
- package/src/web-motion/notifications.ts +67 -0
- package/src/web-motion/select-ripple.ts +23 -0
- package/src/web-motion/statistic.ts +23 -0
- package/src/web-motion/surfaces.ts +137 -0
- package/src/web-motion/tree.ts +44 -0
- package/styles/component-motion.css +39 -0
- package/styles/data-motion.css +4 -0
- package/styles/motion-optional.css +15 -0
- package/styles/motion-presets.css +92 -0
- package/styles/motion-tokens.css +6 -0
- package/styles/motion.css +125 -0
- package/styles/skeleton.css +8 -0
- package/test/component-motion-settings.test.mjs +35 -0
- package/test/content-overlay-motion.test.mjs +36 -0
- package/test/isolation.test.mjs +1 -1
- package/test/motion.test.mjs +215 -0
- package/dist/chunk-GXHANADL.js +0 -1
- /package/dist/{chunk-3PNBQVYS.js → chunk-2BSBXDOT.js} +0 -0
- /package/dist/{chunk-H7ETPJVW.js → chunk-3PODK22R.js} +0 -0
- /package/dist/{chunk-SIU7DSOA.js → chunk-KBWB4OIF.js} +0 -0
- /package/dist/{chunk-OPMD7QNV.js → chunk-W3H7XTA7.js} +0 -0
- /package/dist/{chunk-XWXEH3SW.js → chunk-XVMJG26X.js} +0 -0
package/dist/switch.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./chunk-EPZGLHPD.js";import{b as a}from"./chunk-
|
|
1
|
+
import"./chunk-EPZGLHPD.js";import{b as a}from"./chunk-TZSX4OTV.js";import"./chunk-EOKNLCNS.js";export{a as createSwitch};
|
package/dist/table.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a}from"./chunk-
|
|
1
|
+
import"./chunk-YLFXSHKZ.js";import{b as a}from"./chunk-T45FJNEQ.js";import"./chunk-EOKNLCNS.js";export{a as createTable};
|
package/dist/time-picker.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a}from"./chunk-
|
|
1
|
+
import"./chunk-V4ZQLQ7L.js";import{a}from"./chunk-EPPETNK6.js";import"./chunk-MEEQNPGM.js";import"./chunk-5TEIXA4R.js";import"./chunk-SDMH2EVD.js";export{a as createTimePicker};
|
package/dist/tree-view.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a}from"./chunk-
|
|
1
|
+
import"./chunk-ZDGZGAYG.js";import{a}from"./chunk-3PODK22R.js";import"./chunk-5TEIXA4R.js";import"./chunk-SDMH2EVD.js";export{a as createTreeView};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MotionController } from '../motion/controller.js';
|
|
2
|
+
/** Decorate the selected chip after its real click, even if that click replaces
|
|
3
|
+
* its button. No synthesized activation or application-state change. */
|
|
4
|
+
export declare function enhanceChipRipple(root: HTMLElement, motion: MotionController): {
|
|
5
|
+
destroy(): void;
|
|
6
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MotionController, MotionOptions } from '../motion/controller.js';
|
|
2
|
+
/** One decorative pulse per native activation. Label clicks, Space and radio
|
|
3
|
+
* arrow changes keep the original input, focus, value and change events. */
|
|
4
|
+
export declare function enhanceChoiceRipple(root: HTMLElement, motion: MotionController, timing?: MotionOptions): {
|
|
5
|
+
destroy(): void;
|
|
6
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MotionController } from '../motion/controller.js';
|
|
2
|
+
export type ContentMotionKind = 'profile-card' | 'video-lightbox' | 'video-player' | 'collection-layout' | 'cascader' | 'transfer' | 'chip' | 'tree-select' | 'timeline' | 'skeleton' | 'media-image' | 'tour' | 'notification-center';
|
|
3
|
+
/** DOM-only enhancement: the original component still owns data, focus and media playback. */
|
|
4
|
+
export declare function enhanceContentMotion(root: HTMLElement, motion: MotionController, kind: ContentMotionKind, options?: {
|
|
5
|
+
timelineKeys?: () => string[];
|
|
6
|
+
}): {
|
|
7
|
+
capture: () => void;
|
|
8
|
+
destroy(): void;
|
|
9
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MotionController } from '../motion/controller.js';
|
|
2
|
+
export interface MotionRow {
|
|
3
|
+
key: string;
|
|
4
|
+
element: HTMLElement;
|
|
5
|
+
cells: HTMLElement[];
|
|
6
|
+
cellKeys: string[];
|
|
7
|
+
}
|
|
8
|
+
/** Presentation only. Data, selection, sorting, paging and keyboard handling stay with the component. */
|
|
9
|
+
export declare function enhanceDataRows(root: HTMLElement, motion: MotionController, read: () => MotionRow[]): {
|
|
10
|
+
capture: () => void;
|
|
11
|
+
destroy(): void;
|
|
12
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MotionController } from '../motion/controller.js';
|
|
2
|
+
/** Adds feedback to existing controls without replacing inputs or intercepting native events. */
|
|
3
|
+
export declare function enhanceField(root: HTMLElement, motion: MotionController): {
|
|
4
|
+
sync: () => void;
|
|
5
|
+
destroy(): void;
|
|
6
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MotionController } from '../motion/controller.js';
|
|
2
|
+
/** The existing Sidebar/Pagination still owns navigation, focus and events.
|
|
3
|
+
* This marker only follows aria-current; it never intercepts links or history. */
|
|
4
|
+
export declare function enhanceNavigation(root: HTMLElement, motion: MotionController, kind: 'sidebar' | 'pagination' | 'tabs'): {
|
|
5
|
+
destroy(): void;
|
|
6
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MotionController } from '../motion/controller.js';
|
|
2
|
+
/** Observe the existing message stack. Its runtime still owns timers, pause on
|
|
3
|
+
* hover/focus, announcements and dismissal. Exits are decorative, inert copies. */
|
|
4
|
+
export declare function enhanceNotifications(root: HTMLElement, motion: MotionController): {
|
|
5
|
+
destroy(): void;
|
|
6
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MotionController, MotionOptions } from '../motion/controller.js';
|
|
2
|
+
/** Decorate an actual open/close or selection change, after Select has updated
|
|
3
|
+
* its label. The component continues to own focus, keyboard and value events. */
|
|
4
|
+
export declare function enhanceSelectRipple(root: HTMLElement, motion: MotionController, timing?: MotionOptions): {
|
|
5
|
+
destroy(): void;
|
|
6
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MotionController } from '../motion/controller.js';
|
|
2
|
+
type Values = {
|
|
3
|
+
Value?: number;
|
|
4
|
+
Precision?: number;
|
|
5
|
+
Locale?: string;
|
|
6
|
+
IsLoading?: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** Exact value remains accessible throughout the decorative number transition. */
|
|
9
|
+
export declare function enhanceStatistic(root: HTMLElement, motion: MotionController, read: () => Values): {
|
|
10
|
+
capture(): void;
|
|
11
|
+
destroy(): void;
|
|
12
|
+
};
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MotionController } from '../motion/controller.js';
|
|
2
|
+
/** Existing components own focus trapping, dismissal and events; the first dialog
|
|
3
|
+
* presentation receives named context after paint. A closing surface is an inert,
|
|
4
|
+
* short-lived visual copy; its bounds are frozen so focus restoration cannot reflow it.
|
|
5
|
+
*/
|
|
6
|
+
export declare function observeSurface(root: HTMLElement, motion: MotionController, kind: 'modal' | 'drawer' | 'palette' | 'select' | 'picker' | 'cascader' | 'tree-select'): {
|
|
7
|
+
sync: () => void;
|
|
8
|
+
capture: () => void;
|
|
9
|
+
destroy(): void;
|
|
10
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MotionController } from '../motion/controller.js';
|
|
2
|
+
/** The tree runtime owns hidden/expanded state and keyboard navigation. During a
|
|
3
|
+
* full-motion collapse only, keep the inert visual in layout until it reaches zero. */
|
|
4
|
+
export declare function enhanceTree(root: HTMLElement, motion: MotionController): {
|
|
5
|
+
destroy(): void;
|
|
6
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Community Form & Layout Essentials
|
|
1
|
+
# Community Form & Layout Essentials
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
These 12 controls and 18 native Blocks shipped in Community 1.1.0. The isolated 1.2.0 review candidate adds opt-in motion and retains their existing APIs. Public 1.2 publication is held. This is not a full-library accessibility certification.
|
|
4
4
|
|
|
5
5
|
## Local preview and build
|
|
6
6
|
|
|
@@ -86,7 +86,7 @@ Table columns accept `Key`, `Header`, `Sortable`, `Align`, and optional text-onl
|
|
|
86
86
|
|
|
87
87
|
## Native local implementation and verification boundary
|
|
88
88
|
|
|
89
|
-
Eighteen public native blocks are implemented
|
|
89
|
+
Eighteen public native blocks are implemented in the Service Studio producers for `BluKavantUi_Community_Inputs`, `BluKavantUi_Community_Layout`, and `BluKavantUi_Community_Data`, under `Essentials`. Each block has a custom icon. Local OML review copies, original backups, model-edit source, validation results, and dependency inventories are saved in `native-builds/FormLayoutEssentials20261022/`. These are unpublished development copies; the released OMLs and npm archives are unchanged.
|
|
90
90
|
|
|
91
91
|
All three producers pass Service Studio validation with zero errors and only their two pre-existing license/notice resource warnings. The actual **Remove Unused Dependencies** command was run on each producer; all reported no dependencies to remove. Remaining references are `(System)` and, for Inputs/Data, `BluKavantUi_Community_Core`. There is no OutSystems UI or Material UI dependency. Platform Container, Expression, Image, and Placeholder widgets support editor display; they are distinct from the OutSystems UI producer.
|
|
92
92
|
|
|
@@ -119,4 +119,4 @@ OnReady and OnParametersChanged synchronize each control; OnDestroy disposes it.
|
|
|
119
119
|
|
|
120
120
|
Consumers own values and application validation: assign the returned OnChange value to the variable passed into the block, and pass application validation messages through `Error`. These custom HTML controls do **not** automatically register with OutSystems `Form.Valid`; the consumer must validate its model before saving. Browser constraint validation and the web `validate()` API remain available for ordinary HTML forms.
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
Compiled development consumers have been exercised. See the 1.2 review evidence for exact keyboard, VoiceOver and motion coverage; this does not establish coverage of every consumer application. Public publication remains held. FormBuilder/FormRenderer remain Pro.
|
package/edition.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"edition": "Community",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"license": "BluKavantUI Community Free Use, Official Distribution Licence 1.2",
|
|
5
5
|
"distribution": "Official @blukavantui/community npm package, official BluKavantUI Web downloads and official OutSystems Forge listing when published",
|
|
6
6
|
"redistribution": "No republishing, mirroring or resale of the reusable components, including modified copies; see LICENSE.txt for permitted private work, application delivery and third-party rights.",
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blukavantui/community",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@blukavantui/community",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.2.0",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"ag-grid-community": "36.2.0"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blukavantui/community",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "BluKavantUI Community: 67 Web components. OutSystems integration available separately.",
|
|
5
5
|
"homepage": "https://blukavantui.blutekmedia.com/community",
|
|
6
6
|
"bugs": {
|
|
@@ -414,7 +414,30 @@
|
|
|
414
414
|
"types": "./dist/form-layout.d.ts",
|
|
415
415
|
"import": "./dist/form-layout.js"
|
|
416
416
|
},
|
|
417
|
-
"./form-layout.css": "./dist/form-layout.css"
|
|
417
|
+
"./form-layout.css": "./dist/form-layout.css",
|
|
418
|
+
"./motion": {
|
|
419
|
+
"types": "./dist/motion.d.ts",
|
|
420
|
+
"import": "./dist/motion.js"
|
|
421
|
+
},
|
|
422
|
+
"./motion.css": "./dist/motion.css",
|
|
423
|
+
"./component-motion": {
|
|
424
|
+
"types": "./dist/component-motion.d.ts",
|
|
425
|
+
"import": "./dist/component-motion.js"
|
|
426
|
+
},
|
|
427
|
+
"./data-motion": {
|
|
428
|
+
"types": "./dist/data-motion.d.ts",
|
|
429
|
+
"import": "./dist/data-motion.js"
|
|
430
|
+
},
|
|
431
|
+
"./motion/settings": {
|
|
432
|
+
"types": "./dist/motion-settings.d.ts",
|
|
433
|
+
"import": "./dist/motion-settings.js"
|
|
434
|
+
},
|
|
435
|
+
"./motion/enhancers": {
|
|
436
|
+
"types": "./dist/motion-enhancers.d.ts",
|
|
437
|
+
"import": "./dist/motion-enhancers.js"
|
|
438
|
+
},
|
|
439
|
+
"./component-motion.css": "./dist/component-motion.css",
|
|
440
|
+
"./data-motion.css": "./dist/data-motion.css"
|
|
418
441
|
},
|
|
419
442
|
"files": [
|
|
420
443
|
"dist",
|
|
@@ -432,7 +455,10 @@
|
|
|
432
455
|
"docs/PUBLISHING.md",
|
|
433
456
|
"assets",
|
|
434
457
|
"draft-components.json",
|
|
435
|
-
"docs/FORM-LAYOUT-ESSENTIALS.md"
|
|
458
|
+
"docs/FORM-LAYOUT-ESSENTIALS.md",
|
|
459
|
+
"scripts/build-motion-css.mjs",
|
|
460
|
+
"scripts/build-motion-styles.mjs",
|
|
461
|
+
"scripts/motion-entries.mjs"
|
|
436
462
|
],
|
|
437
463
|
"sideEffects": [
|
|
438
464
|
"**/*.css",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Web-only utility generation from the same presets as WAAPI. Does not touch native/release artifacts. */
|
|
2
|
+
import { build } from 'esbuild';
|
|
3
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
6
|
+
const result = await build({ absWorkingDir: root, entryPoints: ['src/motion/effects.ts'], bundle: true, format: 'esm', platform: 'node', write: false });
|
|
7
|
+
const { motionEffects, motionEasings, motionFrames, motionClass, motionEasingClass, motionOrigins } = await import('data:text/javascript;base64,' + Buffer.from(result.outputFiles[0].text).toString('base64'));
|
|
8
|
+
const lines = ['/* Generated by scripts/build-motion-css.mjs. Edit src/motion/effects.ts, then regenerate. */'];
|
|
9
|
+
for (const effect of motionEffects) {
|
|
10
|
+
for (const phase of ['enter', 'exit']) {
|
|
11
|
+
const name = motionClass(effect, phase);
|
|
12
|
+
const aliases = effect === 'fade' ? [`.blu-motion-fade-${phase === 'enter' ? 'in' : 'out'}`]
|
|
13
|
+
: phase === 'enter' && effect === 'slide-up' ? ['.blu-motion-slide-up']
|
|
14
|
+
: phase === 'enter' && effect === 'zoom-in' ? ['.blu-motion-scale-in'] : [];
|
|
15
|
+
lines.push([`.${name}`, ...aliases].join(', ') + ` { animation: ${name} var(--blu-motion-duration) var(--blu-motion-ease) var(--blu-motion-delay) both; }`);
|
|
16
|
+
const frames = motionFrames(effect, phase, 'var(--blu-motion-distance)');
|
|
17
|
+
lines.push(`@keyframes ${name} { ${frames.map((frame, index) => {
|
|
18
|
+
const percent = (frame.offset ?? index / (frames.length - 1)) * 100;
|
|
19
|
+
return `${percent}% { ${Object.entries(frame).filter(([key]) => key !== 'offset').map(([key, value]) => `${key.replace(/[A-Z]/g, letter => "-" + letter.toLowerCase())}: ${value};`).join(' ')} }`;
|
|
20
|
+
}).join(' ')} }`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
for (const [name, curve] of Object.entries(motionEasings)) lines.push(`.${motionEasingClass(name)} { --blu-motion-ease: ${curve}; }`);
|
|
24
|
+
for (const [name, origin] of Object.entries(motionOrigins)) lines.push(`.blu-motion-origin-${name} { transform-origin: ${origin}; }`);
|
|
25
|
+
// Exit utilities do not manage hidden/inert. Owners must do that after animationend or use createMotionPresence.
|
|
26
|
+
const css = lines.join('\n') + '\n';
|
|
27
|
+
const target = new URL('../styles/motion-presets.css', import.meta.url);
|
|
28
|
+
if (process.argv.includes('--check')) {
|
|
29
|
+
if (await readFile(target, 'utf8') !== css) throw Error('Motion CSS is stale. Run node scripts/build-motion-css.mjs.');
|
|
30
|
+
} else await writeFile(target, css);
|
|
31
|
+
|
|
32
|
+
const tokenBuild = await build({absWorkingDir:root,entryPoints:['src/motion/tokens.ts'],bundle:true,format:'esm',platform:'node',write:false});
|
|
33
|
+
const {motionTiming} = await import('data:text/javascript;base64,'+Buffer.from(tokenBuild.outputFiles[0].text).toString('base64'));
|
|
34
|
+
const tokenCSS = '/* Generated from src/motion/tokens.ts. */\n:root {\n'+['fast','normal','slow'].map(key=>` --blu-motion-${key}: ${motionTiming[key]}ms;`).join('\n')+'\n}\n';
|
|
35
|
+
const tokenTarget = new URL('../styles/motion-tokens.css',import.meta.url);
|
|
36
|
+
if(process.argv.includes('--check')) {if(await readFile(tokenTarget,'utf8')!==tokenCSS)throw Error('Motion tokens CSS is stale.');} else await writeFile(tokenTarget,tokenCSS);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { build } from 'esbuild';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
/** The same stylesheet is used by the package build and local documentation. */
|
|
7
|
+
export async function buildMotionStyles(outdir) {
|
|
8
|
+
const root = fileURLToPath(new URL('../', import.meta.url));
|
|
9
|
+
const result = await build({ absWorkingDir: root, entryPoints: ['styles/motion.css'],
|
|
10
|
+
bundle: true, write: false, target: ['safari16.4', 'chrome110'], legalComments: 'inline' });
|
|
11
|
+
const css = result.outputFiles[0].text;
|
|
12
|
+
await mkdir(outdir, { recursive: true });
|
|
13
|
+
await writeFile(resolve(outdir, 'motion.css'), css);
|
|
14
|
+
return css;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Call after generating the base full stylesheet; used by the normal release build. */
|
|
18
|
+
export async function includeMotionStyles(outdir) {
|
|
19
|
+
const base = await readFile(resolve(outdir, 'styles.css'), 'utf8');
|
|
20
|
+
const motion = await buildMotionStyles(outdir);
|
|
21
|
+
let combined = motion;
|
|
22
|
+
for (const name of ['component-motion', 'data-motion']) {
|
|
23
|
+
const css = await readFile(new URL('../styles/' + name + '.css', import.meta.url), 'utf8');
|
|
24
|
+
await writeFile(resolve(outdir, name + '.css'), css);
|
|
25
|
+
combined += '\n' + css;
|
|
26
|
+
}
|
|
27
|
+
await writeFile(resolve(outdir, 'styles.css'), base + '\n' + combined);
|
|
28
|
+
return motion;
|
|
29
|
+
}
|
package/scripts/build.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import './build-motion-css.mjs';
|
|
2
|
+
import { motionEntries } from './motion-entries.mjs';
|
|
3
|
+
import { includeMotionStyles } from './build-motion-styles.mjs';
|
|
1
4
|
import {build} from 'esbuild';
|
|
2
5
|
import {readFile,writeFile,mkdir,rm,cp} from 'node:fs/promises';
|
|
3
6
|
import {execFileSync} from 'node:child_process';
|
|
@@ -9,7 +12,7 @@ const essentials=['button','badge','chip','chip-group','card','skeleton','empty-
|
|
|
9
12
|
const handwritten=new Set(draft.components.map(c=>c.slug));
|
|
10
13
|
const additions=edition.components.map(slug).filter(n=>!essentials.includes(n)&&n!=='grid-basic'&&!handwritten.has(n));
|
|
11
14
|
const groups=Object.fromEntries(Object.entries(edition.nativeModules).map(([k,v])=>[k.split('_').at(-1).toLowerCase(),v.map(slug)]));
|
|
12
|
-
const entries=[...new Set(['index','grid-basic','essentials','theme','form-layout',...draft.components.map(c=>c.slug),...essentials,...additions,...Object.keys(groups)])];
|
|
15
|
+
const entries=[...new Set(['index','grid-basic','essentials','theme','form-layout',...motionEntries,...draft.components.map(c=>c.slug),...essentials,...additions,...Object.keys(groups)])];
|
|
13
16
|
await rm('dist',{recursive:true,force:true});await mkdir('dist');
|
|
14
17
|
const web=await build({entryPoints:entries.map(n=>'src/'+n+'.ts'),bundle:true,splitting:true,format:'esm',target:'es2020',minify:true,outdir:'dist',metafile:true,preserveSymlinks:true});
|
|
15
18
|
const nativeEntries={'bluui-community':'src/native.ts','bluui-community-grid':'src/native-grid.ts','native/form-layout':'src/native-components/form-layout.ts'};
|
|
@@ -32,6 +35,7 @@ await cp('styles/form-layout.css','dist/form-layout.css');
|
|
|
32
35
|
for(const c of draft.components)await cp(`styles/${c.slug}.css`,`dist/${c.slug}.css`);
|
|
33
36
|
for(const group of new Set(draft.components.map(c=>c.group)))await writeFile('dist/'+group+'.css',await readFile('dist/'+group+'.css','utf8')+'\n'+draftStyles);
|
|
34
37
|
await writeFile('dist/styles.css',await readFile('dist/styles.css','utf8')+'\n'+draftStyles);
|
|
38
|
+
await includeMotionStyles('dist');
|
|
35
39
|
await cp('draft-components.json','dist/draft-components.json');
|
|
36
40
|
const inputs=[...new Set([...Object.keys(web.metafile.inputs),...Object.keys(native.metafile.inputs)])];
|
|
37
41
|
for(const file of inputs)if(!file.startsWith('src/')&&!file.startsWith('node_modules/ag-grid-community/')&&!file.startsWith('node_modules/ag-stack/'))throw Error('Unexpected source in Community bundle: '+file);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** Local, opt-in web integration. Import after the component styles plus component-motion.css.
|
|
2
|
+
* Create the controller on a scope containing every component host (normally document.body).
|
|
3
|
+
* Dispose this collection before disposing its controller. No native entry points import this file.
|
|
4
|
+
*/
|
|
5
|
+
import type { MotionController } from './motion/controller.js';
|
|
6
|
+
import { createComponentMotionScope, validateComponentMotion } from './motion/component-settings.js';
|
|
7
|
+
import type { ComponentMotionSettings, ComponentMotionHandle } from './motion/component-settings.js';
|
|
8
|
+
export type { ComponentMotionSettings, ComponentMotionHandle } from './motion/component-settings.js';
|
|
9
|
+
import { createMotionButton } from './motion/button.js';
|
|
10
|
+
import { createPressFeedback } from './motion/feedback.js';
|
|
11
|
+
import { createModal } from './component-factories/modal.js';
|
|
12
|
+
import { createDrawer } from './component-factories/drawer.js';
|
|
13
|
+
import { createCommandPalette } from './component-factories/command-palette.js';
|
|
14
|
+
import { createTextInput } from './text-input.js';
|
|
15
|
+
import { createTextArea } from './text-area.js';
|
|
16
|
+
import { createCheckbox } from './checkbox.js';
|
|
17
|
+
import { createSwitch } from './switch.js';
|
|
18
|
+
import { createRadioButton } from './radio-button.js';
|
|
19
|
+
import { createRadioGroup } from './radio-group.js';
|
|
20
|
+
import { createSelect } from './select.js';
|
|
21
|
+
import { createFormField } from './form-field.js';
|
|
22
|
+
import { observeSurface } from './web-motion/surfaces.js';
|
|
23
|
+
import { enhanceField } from './web-motion/fields.js';
|
|
24
|
+
import { createDatePicker } from './component-factories/date-picker.js';
|
|
25
|
+
import { createTimePicker } from './component-factories/time-picker.js';
|
|
26
|
+
import { createDateTimePicker } from './component-factories/date-time-picker.js';
|
|
27
|
+
import { createDateRangePicker } from './component-factories/date-range-picker.js';
|
|
28
|
+
import { createTreeView } from './component-factories/tree-view.js';
|
|
29
|
+
import { createNotification } from './component-factories/notification.js';
|
|
30
|
+
import { enhanceTree } from './web-motion/tree.js';
|
|
31
|
+
import { enhanceNotifications } from './web-motion/notifications.js';
|
|
32
|
+
import { createSidebar } from './component-factories/sidebar.js';
|
|
33
|
+
import { createPagination } from './component-factories/pagination.js';
|
|
34
|
+
import { enhanceNavigation } from './web-motion/navigation.js';
|
|
35
|
+
|
|
36
|
+
type Instance = { element: HTMLElement; destroy(): void };
|
|
37
|
+
type Enhancement = { destroy(): void };
|
|
38
|
+
export function createMotionComponents(motion: MotionController, initial: ComponentMotionSettings = {}) {
|
|
39
|
+
let defaults = validateComponentMotion(initial), disposed = false;
|
|
40
|
+
const instances = new Set<Instance>();
|
|
41
|
+
const configurations = new Map<Instance, () => void>();
|
|
42
|
+
function decorate<A extends unknown[], C extends Instance>(factory: (...args: A) => C, enhance: (component: C, local: MotionController) => Enhancement) {
|
|
43
|
+
return (...args: A): C & ComponentMotionHandle => {
|
|
44
|
+
if (disposed) throw Error('Motion components are destroyed.');
|
|
45
|
+
const component = factory(...args);
|
|
46
|
+
const scoped = createComponentMotionScope(component.element, motion, defaults);
|
|
47
|
+
let extra: Enhancement;
|
|
48
|
+
try { extra = enhance(component, scoped.motion); } catch (error) { scoped.destroy(); component.destroy(); throw error; }
|
|
49
|
+
const destroy = component.destroy.bind(component); let dead = false, overrides: ComponentMotionSettings = {};
|
|
50
|
+
const apply = () => scoped.update({ ...defaults, ...overrides });
|
|
51
|
+
const result = Object.assign(component, { setMotion(settings: ComponentMotionSettings = {}) {
|
|
52
|
+
if (dead) throw Error('Component is destroyed.');
|
|
53
|
+
overrides = validateComponentMotion(settings);
|
|
54
|
+
if (overrides.mode === 'inherit') delete overrides.mode;
|
|
55
|
+
apply();
|
|
56
|
+
} });
|
|
57
|
+
component.destroy = () => { if (dead) return; dead = true; extra.destroy(); scoped.destroy(); destroy(); instances.delete(component); configurations.delete(component); };
|
|
58
|
+
configurations.set(component, apply); instances.add(component); return result;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const field = (component: Instance, local: MotionController) => enhanceField(component.element, local);
|
|
62
|
+
const surface = (kind: 'modal' | 'drawer' | 'palette') => (component: Instance, local: MotionController) => observeSurface(component.element, local, kind);
|
|
63
|
+
const picker = (component: Instance, local: MotionController) => observeSurface(component.element, local, 'picker');
|
|
64
|
+
return {
|
|
65
|
+
createSidebar: decorate(createSidebar, (component, local) => enhanceNavigation(component.element, local, 'sidebar')),
|
|
66
|
+
createPagination: decorate(createPagination, (component, local) => enhanceNavigation(component.element, local, 'pagination')),
|
|
67
|
+
createDatePicker: decorate(createDatePicker, picker),
|
|
68
|
+
createTimePicker: decorate(createTimePicker, picker),
|
|
69
|
+
createDateTimePicker: decorate(createDateTimePicker, picker),
|
|
70
|
+
createDateRangePicker: decorate(createDateRangePicker, picker),
|
|
71
|
+
createTreeView: decorate(createTreeView, (component, local) => enhanceTree(component.element, local)),
|
|
72
|
+
createNotification: decorate(createNotification, (component, local) => enhanceNotifications(component.element, local)),
|
|
73
|
+
createButton: decorate(createMotionButton, (component, local) => {
|
|
74
|
+
const button = component.element.querySelector('button')!;
|
|
75
|
+
const press = createPressFeedback(button, local), update = component.update.bind(component);
|
|
76
|
+
component.update = patch => { update(patch); button.classList.add('blu-motion-press'); };
|
|
77
|
+
return press;
|
|
78
|
+
}),
|
|
79
|
+
createModal: decorate(createModal, surface('modal')),
|
|
80
|
+
createDrawer: decorate(createDrawer, surface('drawer')),
|
|
81
|
+
createCommandPalette: decorate(createCommandPalette, surface('palette')),
|
|
82
|
+
createTextInput: decorate(createTextInput, field),
|
|
83
|
+
createTextArea: decorate(createTextArea, field),
|
|
84
|
+
createCheckbox: decorate(createCheckbox, field),
|
|
85
|
+
createSwitch: decorate(createSwitch, field),
|
|
86
|
+
createRadioButton: decorate(createRadioButton, field),
|
|
87
|
+
createRadioGroup: decorate(createRadioGroup, field),
|
|
88
|
+
createFormField: decorate(createFormField, field),
|
|
89
|
+
createSelect: decorate(createSelect, (component, local) => {
|
|
90
|
+
const feedback = field(component, local), menu = observeSurface(component.element, local, 'select');
|
|
91
|
+
return { destroy() { menu.destroy(); feedback.destroy(); } };
|
|
92
|
+
}),
|
|
93
|
+
/** Replace collection defaults. Existing per-component overrides stay in place. */
|
|
94
|
+
configure(settings: ComponentMotionSettings = {}) {
|
|
95
|
+
if (disposed) throw Error('Motion components are destroyed.');
|
|
96
|
+
defaults = validateComponentMotion(settings);
|
|
97
|
+
for (const apply of configurations.values()) apply();
|
|
98
|
+
},
|
|
99
|
+
destroy() { if (disposed) return; disposed = true; for (const component of Array.from(instances).reverse()) component.destroy(); },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Local opt-in data entry. Existing Table/Grid defaults and native entries are unchanged. */
|
|
2
|
+
import { createTable as baseTable, tableRows } from './form-layout/table.js';
|
|
3
|
+
import type { TableOptions, TableEvents } from './form-layout/types.js';
|
|
4
|
+
import { createGridBasic as baseGrid } from './grid-basic.js';
|
|
5
|
+
import type { GridBasicOptions, GridBasicEvents } from './grid-basic.js';
|
|
6
|
+
import type { MotionController } from './motion/controller.js';
|
|
7
|
+
import { enhanceDataRows } from './web-motion/data-rows.js';
|
|
8
|
+
|
|
9
|
+
import { createComponentMotionScope, type ComponentMotionSettings } from './motion/component-settings.js';
|
|
10
|
+
import { createMotionSettingsGroup } from './motion/settings-group.js';
|
|
11
|
+
|
|
12
|
+
export function createMotionData(motion: MotionController, defaults: ComponentMotionSettings = {}) {
|
|
13
|
+
const settings = createMotionSettingsGroup(defaults);
|
|
14
|
+
const instances = new Set<{ destroy(): void }>(); let dead = false;
|
|
15
|
+
function own<C extends { destroy(): void }>(component: C, extra: { destroy(): void }, scope: ReturnType<typeof createComponentMotionScope>) {
|
|
16
|
+
const binding = settings.bind(scope.update);
|
|
17
|
+
const destroy = component.destroy.bind(component); let disposed = false;
|
|
18
|
+
component.destroy = () => { if (disposed) return; disposed = true; binding.destroy(); extra.destroy(); scope.destroy(); destroy(); instances.delete(component); };
|
|
19
|
+
instances.add(component); return Object.assign(component, {setMotion: binding.setMotion});
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
configure(next: ComponentMotionSettings = {}) { if (dead) throw Error('Motion data components are destroyed.'); settings.configure(next); },
|
|
23
|
+
createTable<Row extends object>(host: HTMLElement, initial: TableOptions<Row>, events: TableEvents<Row> = {}) {
|
|
24
|
+
if (dead) throw Error('Motion data components are destroyed.');
|
|
25
|
+
let options = initial;
|
|
26
|
+
function validate(p: TableOptions<Row>) {
|
|
27
|
+
if (!p.RowKey) throw Error('Animated tables require a stable RowKey.');
|
|
28
|
+
const ids = p.Rows.map(row => row[p.RowKey!]);
|
|
29
|
+
if (ids.some(id => !['string', 'number'].includes(typeof id) || String(id) === '') || new Set(ids.map(String)).size !== ids.length) throw Error('Animated tables require unique, nonempty row keys.');
|
|
30
|
+
}
|
|
31
|
+
validate(options);
|
|
32
|
+
const component = baseTable(host, initial, events), update = component.update;
|
|
33
|
+
const scope = createComponentMotionScope(component.element, motion, settings.settings);
|
|
34
|
+
const extra = enhanceDataRows(component.element, scope.motion, () => {
|
|
35
|
+
const rows = options.Loading ? [] : tableRows({ ...options, ...component.getState() }).rows;
|
|
36
|
+
const elements = [...component.element.querySelectorAll<HTMLTableRowElement>('tbody > tr')];
|
|
37
|
+
return rows.map((row, index) => ({ key: String(row[options.RowKey!]), element: elements[index], cells: [...elements[index].cells], cellKeys: [...options.Columns.map(c => c.Key), '__actions'] }));
|
|
38
|
+
});
|
|
39
|
+
component.update = patch => {
|
|
40
|
+
const next = { ...options, ...patch }; validate(next); extra.capture();
|
|
41
|
+
const focused = document.activeElement instanceof HTMLElement && component.element.contains(document.activeElement) ? document.activeElement : null;
|
|
42
|
+
update(patch); options = next;
|
|
43
|
+
if (focused && !focused.isConnected && document.activeElement === document.body) component.element.querySelector<HTMLElement>('.blu-table__scroll')!.focus({ preventScroll: true });
|
|
44
|
+
};
|
|
45
|
+
return own(component, extra, scope);
|
|
46
|
+
},
|
|
47
|
+
createGridBasic(host: HTMLElement, initial: GridBasicOptions, events: GridBasicEvents = {}) {
|
|
48
|
+
if (dead) throw Error('Motion data components are destroyed.');
|
|
49
|
+
const component = baseGrid(host, initial, events), update = component.update;
|
|
50
|
+
const scope = createComponentMotionScope(component.element, motion, settings.settings);
|
|
51
|
+
const extra = enhanceDataRows(component.element, scope.motion, () => [...component.element.querySelectorAll<HTMLElement>('.ag-center-cols-container > .ag-row[row-id], .ag-grid-scrolling-container > .ag-row[row-id]')].map(element => {
|
|
52
|
+
const cells = [...element.querySelectorAll<HTMLElement>('.ag-cell')];
|
|
53
|
+
return { key: element.getAttribute('row-id')!, element, cells, cellKeys: cells.map(cell => cell.getAttribute('col-id')!) };
|
|
54
|
+
}));
|
|
55
|
+
component.update = patch => { extra.capture(); update(patch); };
|
|
56
|
+
return own(component, extra, scope);
|
|
57
|
+
},
|
|
58
|
+
destroy() { if (dead) return; dead = true; for (const component of [...instances]) component.destroy(); },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -33,7 +33,19 @@ export function createRadioGroup(host: HTMLElement, initial: RadioGroupOptions,
|
|
|
33
33
|
let p = { ...initial }, controls: HTMLInputElement[] = [];
|
|
34
34
|
const f = frame(host, p, true), name = p.Name || uid(); const life = lifecycle(f.root, reset);
|
|
35
35
|
function update(patch: Partial<RadioGroupOptions>) {
|
|
36
|
-
life.check();
|
|
36
|
+
life.check();
|
|
37
|
+
// Native bindings resend Options on every value change. Preserve the same
|
|
38
|
+
// inputs when the options are unchanged so keyboard and screen-reader focus
|
|
39
|
+
// stay on the selected choice.
|
|
40
|
+
const replace = !controls.length || (!!patch.Options && (
|
|
41
|
+
patch.Options.length !== p.Options.length || patch.Options.some((option, i) => {
|
|
42
|
+
const previous = p.Options[i];
|
|
43
|
+
return option.Value !== previous.Value || option.Label !== previous.Label ||
|
|
44
|
+
option.Description !== previous.Description || option.Disabled !== previous.Disabled;
|
|
45
|
+
})
|
|
46
|
+
));
|
|
47
|
+
const focusedValue = controls.find(input => input === document.activeElement)?.value;
|
|
48
|
+
p = { ...p, ...patch };
|
|
37
49
|
if (new Set(p.Options.map(o => o.Value)).size !== p.Options.length) throw Error('Radio option values must be unique.');
|
|
38
50
|
f.body.className = 'blu-radio-group blu-radio-group--' + (p.Direction || 'Vertical').toLowerCase();
|
|
39
51
|
if (replace) {
|
|
@@ -58,6 +70,10 @@ export function createRadioGroup(host: HTMLElement, initial: RadioGroupOptions,
|
|
|
58
70
|
attr(input, 'aria-readonly', p.ReadOnly ? 'true' : undefined);
|
|
59
71
|
if (p.Options[i].Description) attr(input, 'aria-describedby', [input.getAttribute('aria-describedby'), input.id + '-description'].filter(Boolean).join(' '));
|
|
60
72
|
});
|
|
73
|
+
if (replace && focusedValue !== undefined) {
|
|
74
|
+
(controls.find(input => input.value === focusedValue && !input.disabled) ||
|
|
75
|
+
controls.find(input => input.checked && !input.disabled) || controls.find(input => !input.disabled))?.focus();
|
|
76
|
+
}
|
|
61
77
|
}
|
|
62
78
|
function reset() { update({ Value: initial.Value || '' }); } update({});
|
|
63
79
|
return { element: f.root, update, reset, getValue: () => controls.find(c => c.checked)?.value || '',
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { MotionController } from './controller.js';
|
|
2
|
+
|
|
3
|
+
export interface ActionMessages { pending: string; success: string; error: string }
|
|
4
|
+
export type ActionResult = 'success' | 'error' | 'busy' | 'cancelled';
|
|
5
|
+
|
|
6
|
+
/** Feedback follows an actual action promise. No simulated success, timed reset,
|
|
7
|
+
* duplicated live region or overlapping action. The caller owns data and controls. */
|
|
8
|
+
export function createActionFeedback(status: HTMLElement, motion: MotionController) {
|
|
9
|
+
const original = { role: status.getAttribute('role'), atomic: status.getAttribute('aria-atomic'), state: status.getAttribute('data-action-state') };
|
|
10
|
+
status.setAttribute('role', 'status'); status.setAttribute('aria-atomic', 'true');
|
|
11
|
+
let dead = false, pending = false;
|
|
12
|
+
const show = (text: string, state: string) => {
|
|
13
|
+
status.textContent = text; status.dataset.actionState = state;
|
|
14
|
+
void motion.play(status, 'fade', { duration: 350, easing: 'ease-out' });
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
get pending() { return pending; },
|
|
18
|
+
async run(action: () => unknown | Promise<unknown>, messages: ActionMessages): Promise<ActionResult> {
|
|
19
|
+
if (dead) return 'cancelled';
|
|
20
|
+
if (pending) return 'busy';
|
|
21
|
+
pending = true; show(messages.pending, 'pending');
|
|
22
|
+
try {
|
|
23
|
+
await action();
|
|
24
|
+
if (dead) return 'cancelled';
|
|
25
|
+
show(messages.success, 'success'); return 'success';
|
|
26
|
+
} catch {
|
|
27
|
+
if (dead) return 'cancelled';
|
|
28
|
+
show(messages.error, 'error'); return 'error';
|
|
29
|
+
} finally { pending = false; }
|
|
30
|
+
},
|
|
31
|
+
destroy() {
|
|
32
|
+
if (dead) return; dead = true; motion.cancel(status);
|
|
33
|
+
for (const [key, value] of [['role', original.role], ['aria-atomic', original.atomic], ['data-action-state', original.state]]) {
|
|
34
|
+
if (value === null) status.removeAttribute(key!); else status.setAttribute(key!, value!);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { preserveStyles } from './structure.js';
|
|
2
|
+
|
|
3
|
+
export interface BorderGlowOptions {
|
|
4
|
+
/** One slow circuit in milliseconds, 4–60 seconds. Default: 12 seconds. */
|
|
5
|
+
duration?: number;
|
|
6
|
+
paused?: boolean;
|
|
7
|
+
}
|
|
8
|
+
const owners = new WeakSet<HTMLElement>();
|
|
9
|
+
function duration(value: number = 12000) {
|
|
10
|
+
if (!Number.isFinite(value) || value < 4000 || value > 60000) throw Error('Border glow duration must be from 4000 to 60000 ms.');
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
/** Decorative, noninteractive border only. CSS honors the motion scope and OS.
|
|
14
|
+
* Pause automatically while outside the viewport or in a hidden document. */
|
|
15
|
+
export function createBorderGlow(element: HTMLElement, options: BorderGlowOptions = {}) {
|
|
16
|
+
let time = duration(options.duration), paused = options.paused ?? false, visible = true, dead = false;
|
|
17
|
+
if (owners.has(element)) throw Error('This element already has a border glow.');
|
|
18
|
+
const doc = element.ownerDocument, win = doc.defaultView;
|
|
19
|
+
if (!win || !element.isConnected) throw Error('Mount a border glow on a connected browser element.');
|
|
20
|
+
const restore = preserveStyles(element, ['position']);
|
|
21
|
+
if (win.getComputedStyle(element).position === 'static') element.style.position = 'relative';
|
|
22
|
+
const overlay = doc.createElement('span'), rim = doc.createElement('span'), beam = doc.createElement('span');
|
|
23
|
+
overlay.className = 'blu-motion-border-glow'; rim.className = 'blu-motion-border-glow__rim';
|
|
24
|
+
beam.className = 'blu-motion-border-glow__beam'; rim.append(beam);
|
|
25
|
+
overlay.setAttribute('aria-hidden', 'true'); overlay.inert = true; overlay.append(rim); element.append(overlay); owners.add(element);
|
|
26
|
+
const sync = () => { overlay.style.setProperty('--blu-glow-duration', time + 'ms'); overlay.dataset.paused = String(paused || !visible || doc.hidden); };
|
|
27
|
+
const measure = () => overlay.style.setProperty('--blu-glow-size', Math.ceil(Math.hypot(element.offsetWidth + 4, element.offsetHeight + 4)) + 'px');
|
|
28
|
+
const resize = new ResizeObserver(measure); resize.observe(element); measure();
|
|
29
|
+
const intersection = typeof IntersectionObserver === 'function' ? new IntersectionObserver(entries => { visible = entries[0].isIntersecting; sync(); }) : null;
|
|
30
|
+
intersection?.observe(element); doc.addEventListener('visibilitychange', sync); sync();
|
|
31
|
+
return {
|
|
32
|
+
element: overlay,
|
|
33
|
+
update(next: BorderGlowOptions) {
|
|
34
|
+
if (dead) throw Error('Border glow is destroyed.');
|
|
35
|
+
if (next.duration !== undefined) time = duration(next.duration);
|
|
36
|
+
if (next.paused !== undefined) paused = next.paused;
|
|
37
|
+
sync();
|
|
38
|
+
},
|
|
39
|
+
destroy() { if (dead) return; dead = true; resize.disconnect(); intersection?.disconnect(); doc.removeEventListener('visibilitychange', sync); overlay.remove(); restore(); owners.delete(element); },
|
|
40
|
+
};
|
|
41
|
+
}
|