@inc2734/unitone-css 1.4.0 → 1.4.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.
Files changed (35) hide show
  1. package/dist/app.css +1 -1
  2. package/dist/app.js +1 -1
  3. package/dist/behaviors/dividers.js +1 -1
  4. package/dist/behaviors/index.js +1 -1
  5. package/dist/behaviors/stairs.js +1 -1
  6. package/dist/layout-primitives/cluster/react.js +1 -1
  7. package/dist/layout-primitives/index.js +1 -1
  8. package/dist/layout-primitives/marquee/behavior.js +1 -1
  9. package/dist/layout-primitives/marquee/react.js +1 -1
  10. package/dist/layout-primitives/responsive-grid/react.js +1 -1
  11. package/dist/layout-primitives/stack/react.js +1 -1
  12. package/dist/layout-primitives/switcher/react.js +1 -1
  13. package/dist/layout-primitives/vertical-writing/behavior.js +1 -1
  14. package/dist/layout-primitives/vertical-writing/react.js +1 -1
  15. package/dist/layout-primitives/with-sidebar/react.js +1 -1
  16. package/dist/library.js +1 -1
  17. package/package.json +2 -2
  18. package/src/behaviors/dividers.js +3 -1
  19. package/src/behaviors/stairs.js +3 -1
  20. package/src/layout-behavior-state.js +41 -0
  21. package/src/layout-primitives/cluster/index.jsx +25 -16
  22. package/src/layout-primitives/marquee/_index.scss +15 -8
  23. package/src/layout-primitives/marquee/behavior.js +1 -1
  24. package/src/layout-primitives/marquee/index.jsx +69 -7
  25. package/src/layout-primitives/marquee/layout.js +163 -0
  26. package/src/layout-primitives/responsive-grid/index.jsx +29 -17
  27. package/src/layout-primitives/stack/index.jsx +25 -16
  28. package/src/layout-primitives/switcher/index.jsx +26 -17
  29. package/src/layout-primitives/vertical-writing/behavior.js +3 -1
  30. package/src/layout-primitives/vertical-writing/index.jsx +11 -11
  31. package/src/layout-primitives/with-sidebar/index.jsx +27 -18
  32. package/src/library.js +379 -442
  33. package/src/observer-scope.js +53 -0
  34. package/src/register-layout-initializer.js +64 -22
  35. package/src/use-layout-behaviors.js +60 -0
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Owns the resources and queued work of one layout observer initialization.
3
+ *
4
+ * @param {Element} target Target element.
5
+ * @returns {object} Observer lifetime and cleanup helpers.
6
+ */
7
+ export const createObserverScope = (target) => {
8
+ const defaultView = target.ownerDocument.defaultView;
9
+ const cleanups = new Set();
10
+ const frames = new Set();
11
+ let disposed = false;
12
+
13
+ return {
14
+ get disposed() {
15
+ return disposed;
16
+ },
17
+ addCleanup(cleanup) {
18
+ if (disposed) {
19
+ cleanup();
20
+ } else {
21
+ cleanups.add(cleanup);
22
+ }
23
+ },
24
+ requestAnimationFrame(callback) {
25
+ if (disposed) {
26
+ return 0;
27
+ }
28
+ if (!defaultView?.requestAnimationFrame) {
29
+ callback();
30
+ return 0;
31
+ }
32
+
33
+ const id = defaultView.requestAnimationFrame((time) => {
34
+ frames.delete(id);
35
+ if (!disposed) {
36
+ callback(time);
37
+ }
38
+ });
39
+ frames.add(id);
40
+ return id;
41
+ },
42
+ dispose() {
43
+ if (disposed) {
44
+ return;
45
+ }
46
+ disposed = true;
47
+ frames.forEach((id) => defaultView.cancelAnimationFrame(id));
48
+ frames.clear();
49
+ cleanups.forEach((cleanup) => cleanup());
50
+ cleanups.clear();
51
+ },
52
+ };
53
+ };
@@ -22,29 +22,29 @@ const getRegistry = () => {
22
22
  return globalThis[INITIALIZER_REGISTRY_KEY];
23
23
  };
24
24
 
25
- const initializeInitializer = (initializer, root) => {
26
- if (!initializer.enabled() || root?.nodeType !== Node.ELEMENT_NODE) {
27
- return;
28
- }
29
-
30
- const targets = new Set();
31
-
32
- if (root.matches?.(initializer.selector)) {
33
- targets.add(root);
34
- }
35
-
36
- root.querySelectorAll?.(initializer.selector).forEach((target) => {
37
- targets.add(target);
38
- });
39
-
40
- targets.forEach((target) => {
25
+ const initializeTarget = (initializer, target) => {
26
+ if (!target?.isConnected || target.nodeType !== Node.ELEMENT_NODE) return;
27
+ if (!initializer.enabled() || !target.matches?.(initializer.selector)) {
41
28
  if (initializer.initialized.has(target)) {
42
- return;
29
+ initializer.cleanups.get(target)?.();
30
+ initializer.cleanups.delete(target);
31
+ initializer.initialized.delete(target);
32
+ initializer.reset?.(target);
43
33
  }
34
+ return;
35
+ }
36
+ if (initializer.initialized.has(target)) return;
37
+ const cleanup = initializer.initialize(target);
38
+ initializer.initialized.add(target);
39
+ if (typeof cleanup === 'function') initializer.cleanups.set(target, cleanup);
40
+ };
44
41
 
45
- initializer.initialize(target);
46
- initializer.initialized.add(target);
47
- });
42
+ const initializeInitializer = (initializer, root) => {
43
+ if (!root?.isConnected || root.nodeType !== Node.ELEMENT_NODE) return;
44
+ initializeTarget(initializer, root);
45
+ root
46
+ .querySelectorAll?.(initializer.selector)
47
+ .forEach((target) => initializeTarget(initializer, target));
48
48
  };
49
49
 
50
50
  const initializeNode = (root) => {
@@ -58,6 +58,27 @@ const initializeNode = (root) => {
58
58
  });
59
59
  };
60
60
 
61
+ // Inspect detached subtrees without relying on selectors that may have changed before removal.
62
+ const disposeNode = (root) => {
63
+ const registry = getRegistry();
64
+ if (!registry || root?.isConnected || root?.nodeType !== Node.ELEMENT_NODE) {
65
+ return;
66
+ }
67
+
68
+ [root, ...root.querySelectorAll('*')].forEach((target) => {
69
+ registry.initializers.forEach((initializer) => {
70
+ const cleanup = initializer.cleanups?.get(target);
71
+ if (!cleanup || target.isConnected) {
72
+ return;
73
+ }
74
+
75
+ initializer.cleanups.delete(target);
76
+ initializer.initialized.delete(target);
77
+ cleanup();
78
+ });
79
+ });
80
+ };
81
+
61
82
  const flushPendingNodes = () => {
62
83
  const registry = getRegistry();
63
84
  if (!registry) {
@@ -82,25 +103,44 @@ const observeDocument = () => {
82
103
 
83
104
  registry.observer = new MutationObserver((entries) => {
84
105
  entries.forEach((entry) => {
106
+ if (entry.type === 'attributes') {
107
+ registry.initializers.forEach((initializer) => initializeTarget(initializer, entry.target));
108
+ return;
109
+ }
110
+ entry.removedNodes.forEach(disposeNode);
85
111
  entry.addedNodes.forEach((addedNode) => {
86
- if (addedNode?.nodeType === Node.ELEMENT_NODE) {
112
+ if (addedNode?.isConnected && addedNode.nodeType === Node.ELEMENT_NODE) {
87
113
  registry.pendingNodes.add(addedNode);
88
114
  }
89
115
  });
90
116
  });
91
117
 
118
+ registry.pendingNodes.forEach((node) => {
119
+ if (!node.isConnected) {
120
+ registry.pendingNodes.delete(node);
121
+ }
122
+ });
123
+
92
124
  if (!registry.rafId && 0 < registry.pendingNodes.size) {
93
125
  registry.rafId = requestAnimationFrame(flushPendingNodes);
94
126
  }
95
127
  });
96
128
 
97
129
  registry.observer.observe(document.body, {
130
+ attributes: true,
131
+ attributeFilter: ['data-unitone-layout', 'data-unitone-react-layout'],
98
132
  childList: true,
99
133
  subtree: true,
100
134
  });
101
135
  };
102
136
 
103
- export const registerLayoutInitializer = ({ key, selector, initialize, enabled = () => true }) => {
137
+ export const registerLayoutInitializer = ({
138
+ key,
139
+ selector,
140
+ initialize,
141
+ reset,
142
+ enabled = () => true,
143
+ }) => {
104
144
  const registry = getRegistry();
105
145
  if (!registry || registry.initializers.has(key)) {
106
146
  return;
@@ -109,8 +149,10 @@ export const registerLayoutInitializer = ({ key, selector, initialize, enabled =
109
149
  const initializer = {
110
150
  selector,
111
151
  initialize,
152
+ reset,
112
153
  enabled,
113
154
  initialized: new WeakSet(),
155
+ cleanups: new WeakMap(),
114
156
  };
115
157
 
116
158
  registry.initializers.set(key, initializer);
@@ -0,0 +1,60 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useImperativeHandle, useLayoutEffect, useRef } from 'react';
4
+ import { dividersResizeObserver, stairsResizeObserver, verticalsResizeObserver } from './library';
5
+ import { layoutRefreshEvent, resetLayoutBehavior } from './layout-behavior-state';
6
+
7
+ const useBrowserLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
8
+ const observers = {
9
+ divider: dividersResizeObserver,
10
+ stairs: stairsResizeObserver,
11
+ vertical: verticalsResizeObserver,
12
+ };
13
+
14
+ /** React owns observer lifetimes; calculations and resize handling remain shared with HTML. */
15
+ export const useLayoutBehaviors = (enabled, forwardedRef) => {
16
+ const targetRef = useRef(null);
17
+ const lifetimeRef = useRef(null);
18
+ useImperativeHandle(forwardedRef, () => targetRef.current);
19
+
20
+ const dispose = () => {
21
+ const lifetime = lifetimeRef.current;
22
+ if (!lifetime) return;
23
+ lifetime.target.removeEventListener(layoutRefreshEvent, lifetime.refresh);
24
+ lifetime.controllers.forEach((controller, name) => {
25
+ controller.dispose();
26
+ resetLayoutBehavior(lifetime.target, name);
27
+ });
28
+ lifetimeRef.current = null;
29
+ };
30
+
31
+ useBrowserLayoutEffect(() => dispose, []);
32
+ useBrowserLayoutEffect(() => {
33
+ const target = targetRef.current;
34
+ if (lifetimeRef.current?.target !== target) dispose();
35
+ if (!target) return;
36
+ if (!lifetimeRef.current) {
37
+ const controllers = new Map();
38
+ const refresh = () => controllers.forEach((controller) => controller.refresh());
39
+ lifetimeRef.current = { target, controllers, refresh };
40
+ target.addEventListener(layoutRefreshEvent, refresh);
41
+ }
42
+ const { controllers } = lifetimeRef.current;
43
+ for (const [name, observe] of Object.entries(observers)) {
44
+ if (!enabled[name]) {
45
+ if (controllers.has(name)) {
46
+ controllers.get(name).dispose();
47
+ controllers.delete(name);
48
+ resetLayoutBehavior(target, name);
49
+ }
50
+ } else if (!controllers.has(name)) {
51
+ const stop = observe(target, { reactOwned: true });
52
+ controllers.set(name, { dispose: stop, refresh: stop.refresh });
53
+ } else {
54
+ controllers.get(name).refresh();
55
+ }
56
+ }
57
+ });
58
+
59
+ return targetRef;
60
+ };