@wwog/react 1.3.0 → 1.3.1

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/README.md CHANGED
@@ -313,6 +313,69 @@ function Example() {
313
313
  - `format`: Optional function to format the date, defaults to `toLocaleString()`.
314
314
  - `children`: Function to render the formatted date, receives the formatted date as an argument.
315
315
 
316
+ #### `<Observer>` (v1.3.1+)
317
+
318
+ A declarative Intersection Observer component for lazy loading, infinite scrolling, and viewport-based interactions.
319
+
320
+ ```tsx
321
+ import { Observer } from "@wwog/react";
322
+
323
+ function LazyImage({ src, alt }) {
324
+ const [isVisible, setIsVisible] = useState(false);
325
+
326
+ return (
327
+ <Observer
328
+ onIntersect={(entry) => {
329
+ if (entry.isIntersecting) {
330
+ setIsVisible(true);
331
+ }
332
+ }}
333
+ threshold={0.1}
334
+ triggerOnce
335
+ >
336
+ <div className="image-container">
337
+ {isVisible ? (
338
+ <img src={src} alt={alt} />
339
+ ) : (
340
+ <div className="placeholder">Loading...</div>
341
+ )}
342
+ </div>
343
+ </Observer>
344
+ );
345
+ }
346
+
347
+ // Infinite scrolling example
348
+ function InfiniteList({ items, onLoadMore }) {
349
+ return (
350
+ <div>
351
+ {items.map((item) => (
352
+ <div key={item.id}>{item.content}</div>
353
+ ))}
354
+ <Observer
355
+ onIntersect={(entry) => {
356
+ if (entry.isIntersecting) {
357
+ onLoadMore();
358
+ }
359
+ }}
360
+ rootMargin="100px"
361
+ >
362
+ <div>Loading more...</div>
363
+ </Observer>
364
+ </div>
365
+ );
366
+ }
367
+ ```
368
+
369
+ - `onIntersect`: Callback function triggered when intersection changes, receives IntersectionObserverEntry as parameter.
370
+ - `threshold`: Intersection threshold, can be a number (0-1) or array of numbers, defaults to 0.
371
+ - `root`: Root element for intersection observation, defaults to viewport.
372
+ - `rootMargin`: Root margin for expanding/shrinking the root's bounding box, defaults to "0px".
373
+ - `triggerOnce`: Whether to trigger only once, defaults to false.
374
+ - `disabled`: Whether to disable observation, defaults to false.
375
+ - `children`: Child elements to observe.
376
+ - `className`: CSS class name for the wrapper element.
377
+ - `style`: Inline styles for the wrapper element.
378
+
316
379
  #### `<SizeBox>`
317
380
 
318
381
  Create a fixed-size container for layout adjustment and spacing control.
package/dist/index.d.mts CHANGED
@@ -372,6 +372,95 @@ interface ToggleProps<T = boolean> {
372
372
  */
373
373
  declare const Toggle: <T>(props: ToggleProps<T>) => React$1.ReactNode;
374
374
 
375
+ interface ObserverProps {
376
+ /**
377
+ * @description_en Callback function when intersection occurs.
378
+ * @description_zh 交叉时触发的回调函数。
379
+ */
380
+ onIntersect: (entry: IntersectionObserverEntry, observer: IntersectionObserver) => void;
381
+ /**
382
+ * @description_en Threshold(s) at which to trigger the callback. Default is 0.1.
383
+ * @description_zh 触发回调的阈值,默认为 0.1。
384
+ * @optional
385
+ * @default 0.1
386
+ */
387
+ threshold?: number | number[];
388
+ /**
389
+ * @description_en The root element for intersection. Default is viewport.
390
+ * @description_zh 交叉的根元素,默认为视口。
391
+ * @optional
392
+ * @default null
393
+ */
394
+ root?: Element | Document | null;
395
+ /**
396
+ * @description_en Margin around the root. Default is "0px".
397
+ * @description_zh 根元素周围的边距,默认为 "0px"。
398
+ * @optional
399
+ * @default "0px"
400
+ */
401
+ rootMargin?: string;
402
+ /**
403
+ * @description_en Whether to trigger only once. Default is false.
404
+ * @description_zh 是否只触发一次,默认为 false。
405
+ * @optional
406
+ * @default false
407
+ */
408
+ triggerOnce?: boolean;
409
+ /**
410
+ * @description_en Whether to disable the observer. Default is false.
411
+ * @description_zh 是否禁用观察者,默认为 false。
412
+ * @optional
413
+ * @default false
414
+ */
415
+ disabled?: boolean;
416
+ /**
417
+ * @description_en Child elements to observe.
418
+ * @description_zh 要观察的子元素。
419
+ * @optional
420
+ */
421
+ children?: ReactNode;
422
+ /**
423
+ * @description_en CSS class name.
424
+ * @description_zh CSS 类名。
425
+ * @optional
426
+ */
427
+ className?: string;
428
+ /**
429
+ * @description_en Inline styles.
430
+ * @description_zh 内联样式。
431
+ * @optional
432
+ */
433
+ style?: React$1.CSSProperties;
434
+ }
435
+ /**
436
+ * @description_zh 交叉观察者组件,用于监听元素与视口的交叉状态,常用于懒加载和无限滚动场景。
437
+ * @description_en Intersection Observer component for monitoring element-viewport intersection, commonly used for lazy loading and infinite scrolling.
438
+ * @component
439
+ * @example
440
+ * ```tsx
441
+ * // 懒加载示例
442
+ * <Observer onIntersect={loadImage} triggerOnce>
443
+ * <img data-src="image.jpg" alt="Lazy loaded" />
444
+ * </Observer>
445
+ *
446
+ * // 无限滚动示例
447
+ * <Observer onIntersect={loadMore} threshold={0.1}>
448
+ * <div>滚动到这里加载更多</div>
449
+ * </Observer>
450
+ *
451
+ * // 自定义根元素和边距
452
+ * <Observer
453
+ * onIntersect={handleIntersect}
454
+ * root={scrollContainer}
455
+ * rootMargin="100px"
456
+ * threshold={[0, 0.5, 1]}
457
+ * >
458
+ * <div>观察目标</div>
459
+ * </Observer>
460
+ * ```
461
+ */
462
+ declare const Observer: React$1.FC<ObserverProps>;
463
+
375
464
  interface ArrayRenderProps<T> {
376
465
  items: T[];
377
466
  renderItem: (item: T, index: number) => React$1.ReactNode;
@@ -613,5 +702,5 @@ declare class Counter {
613
702
 
614
703
  declare const safePromiseTry: <T, U extends unknown[]>(callbackFn: (...args: U) => T | PromiseLike<T>, ...args: U) => Promise<Awaited<T>>;
615
704
 
616
- export { ArrayRender, Counter, DateRender, False, If, Pipe, Scope, SizeBox, Styles, Switch, Toggle, True, When, childrenLoop, createExternalState, cx, formatDate, safePromiseTry, useControlled };
617
- export type { ArrayRenderProps, CreateStateListener, CxInput, DateRenderProps, ElseIfProps, ElseProps, ExternalSideEffect, ExternalState, ExternalStateOptions, ExternalWithKernel, FalseProps, IfProps, PipeProps, ScopeProps, StylesDescriptor, StylesProps, StylesType, SwitchCaseProps, SwitchDefaultProps, SwitchProps, ThenProps, ToggleProps, Transform, TrueProps, UseControlledOptions, WhenProps };
705
+ export { ArrayRender, Counter, DateRender, False, If, Observer, Pipe, Scope, SizeBox, Styles, Switch, Toggle, True, When, childrenLoop, createExternalState, cx, formatDate, safePromiseTry, useControlled };
706
+ export type { ArrayRenderProps, CreateStateListener, CxInput, DateRenderProps, ElseIfProps, ElseProps, ExternalSideEffect, ExternalState, ExternalStateOptions, ExternalWithKernel, FalseProps, IfProps, ObserverProps, PipeProps, ScopeProps, StylesDescriptor, StylesProps, StylesType, SwitchCaseProps, SwitchDefaultProps, SwitchProps, ThenProps, ToggleProps, Transform, TrueProps, UseControlledOptions, WhenProps };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import r,{useMemo as h,Children as J,Fragment as S,isValidElement as _,cloneElement as R,useEffect as W,useState as x,useCallback as H}from"react";function C(t,n){if(t===void 0)return;let e=0;if(Array.isArray(t)){for(const l of t)if(n(l,e++)===!1)break}else n(t,e)}const B=(t,n)=>t===n,E=t=>r.createElement(r.Fragment,null,t.children);E.displayName="Switch_Case";const w=t=>r.createElement(r.Fragment,null,t.children);w.displayName="Switch_Default";const y=t=>{const{value:n,compare:e=B,children:l,strict:o=!1}=t,a=new Set;let s=null,f=null,m=!1;return C(l,(i,c)=>{if(!r.isValidElement(i))throw new Error(`Switch Children only accepts valid React elements at index ${c}`);const u=i.type;if(u.displayName===E.displayName){const d=i.props;if(a.has(d.value))throw new Error(`Switch found duplicate Case value at index ${c}: ${JSON.stringify(d.value)}${o?" (detected in strict mode)":""}`);if(a.add(d.value),!s&&e(n,d.value)&&(s=d.children,o===!1))return!1}else if(u.displayName===w.displayName){if(m)throw new Error(`Switch can only have one Default child at index ${c}`);if(m=!0,f=i.props.children,!o&&s)return!1}else throw new Error(`Switch Children only accepts 'Case' or 'Default' elements, found: ${String(u.displayName||u.name||u)} at index ${c}`)}),r.createElement(r.Fragment,null,s??f)};y.displayName="Switch",y.Case=E,y.Default=w,y.createTyped=function(){return{Switch:y,Case:E,Default:w}};const N=t=>r.createElement(r.Fragment,null,t.children),v=({children:t})=>r.createElement(r.Fragment,null,t),F=t=>r.createElement(r.Fragment,null,t.children);N.displayName="If_Then",v.displayName="If_Else",F.displayName="If_ElseIf";const p=({condition:t,children:n})=>{let e=null,l=null;const o=[];if(r.Children.forEach(n,a=>{if(!r.isValidElement(a))throw new Error("If component only accepts valid React elements");const s=a.type;if(s.displayName===N.displayName){if(e)throw new Error("If component can only have one Then child");e=a}else if(s.displayName===F.displayName)o.push(a);else if(s.displayName===v.displayName){if(l)throw new Error("If component can only have one Else child");l=a}else throw new Error(`If component only accepts 'Then', 'ElseIf', or 'Else' elements as children, found: ${String(s.displayName||s.name||s)}`)}),t)return e?r.createElement(r.Fragment,null,e.props.children):null;for(const a of o)if(a.props.condition)return r.createElement(r.Fragment,null,a.props.children);return l?r.createElement(r.Fragment,null,l.props.children):null};p.displayName="If",p.Then=N,p.ElseIf=F,p.Else=v,p.createTyped=function(){return{If:p,Then:N,ElseIf:F,Else:v}};const V=({condition:t,children:n})=>t?r.createElement(r.Fragment,null,n):null,Z=({condition:t,children:n})=>t===!1?r.createElement(r.Fragment,null,n):null,z=({all:t,any:n,none:e,children:l,fallback:o})=>h(()=>(t&&(n||e)&&console.warn('When: Multiple condition types (all, any, none) provided; "all" takes precedence.'),!!(t&&t.length>0&&t.every(Boolean)||n&&n.length>0&&n.some(Boolean)||e&&e.length>0&&e.every(a=>!a))),[t,n,e])?r.createElement(r.Fragment,null,l):r.createElement(r.Fragment,null,o||null),L=({data:t,transform:n,render:e,fallback:l})=>{const o=h(()=>n.reduce((a,s)=>s(a),t),[t,n]);return o==null?r.createElement(r.Fragment,null,l||null):r.createElement(r.Fragment,null,e(o))},G=t=>{const{children:n,h:e,w:l,size:o,height:a,width:s,className:f}=t;return r.createElement("div",{style:{width:o||l||s,height:o||e||a,flexShrink:0},className:f},n)},U=({let:t,props:n,children:e,fallback:l})=>{const o=h(()=>typeof t=="function"?t(n):t,[t,n]);return!e||!Object.keys(o).length?r.createElement(r.Fragment,null,l||null):r.createElement(r.Fragment,null,e(o))};function M(...t){const n=new Set;for(const e of t)if(e){if(typeof e=="string")n.add(e);else if(Array.isArray(e))e.forEach(l=>n.add(l));else if(typeof e=="object")for(const[l,o]of Object.entries(e))o&&n.add(l)}return Array.from(n).join(" ")}const q=t=>typeof t=="object"&&!!t,b=({className:t,children:n,asWrapper:e=!1})=>{if(!n)return null;if(J.count(n)>1)return console.error("<Styles>: children has more than one child. Please check your code."),r.createElement(S,null,n);if(!t)return r.createElement(S,null,n);const l=typeof t=="string"?t:M(...Object.values(t));if(e)return r.createElement(e===!0?"div":e,{className:l},n);if(_(n)){const o=n;let a=o?.props?.className;return o?.type?.displayName===b.displayName&&q(a)&&(a=M(...Object.values(a))),R(n,{className:M(l,a)})}return console.error("<Styles>: children is not a valid React element. Please check your code."),r.createElement(S,null,n)};b.displayName="W/Styles";const K=t=>{const{index:n=0,options:e,next:l,render:o}=t;W(()=>{if(e.length<n+1)throw new Error(`Index ${n} is out of bounds for options array of length ${e.length}. Defaulting to first option.`)},[n,e]);const[a,s]=x(n),f=()=>{s(m=>e.length?l?l(m,e):(m+1)%e.length:m)};return o(e[a],f)};function Q(t){const{items:n,renderItem:e,filter:l}=t;return n?r.createElement(S,null,n.map((o,a)=>l&&!l(o)?null:e(o,a))):(console.error("ArrayRender: items is null"),null)}function X({source:t,format:n,children:e}){const l=h(()=>{if(t instanceof Date)return t;if(typeof t=="string"||typeof t=="number"){const a=new Date(t);return isNaN(a.getTime())?null:a}return null},[t]),o=h(()=>l?n?n(l):l.toLocaleString():null,[l,n]);return!o||!e?null:r.createElement(r.Fragment,null,e(o))}const $="onChange",ee="value";function te(t){const{defaultValue:n,onBeforeChange:e,trigger:l=$,valuePropName:o=ee,props:a}=t,s=Object.prototype.hasOwnProperty.call(a,o),[f,m]=x(n),i=s?a[o]:f,c=h(()=>a[l],[a,l]),u=H(d=>{const g=typeof d=="function"?d(i):d;e&&e(g,i)===!1||(s||m(g),c&&c(g))},[s,e,i,c]);return[i,u]}function ne(t,...n){try{const e=t(...n);return e instanceof Promise?e:Promise.resolve(e)}catch(e){return Promise.reject(e)}}const I=typeof Promise.try=="function"?Promise.try.bind(Promise):ne;function re(t,n={}){let e=typeof t=="function"?t():t;const l=[],{sideEffect:o,transform:a}=n,s=()=>{const i=e;return a?.get?a.get(i):i},f=i=>{const c=e,u=a?.get?a.get(c):c;e=a?.set?a.set(typeof i=="function"?i(u):i):typeof i=="function"?i(u):i,l.forEach(d=>d(e)),o&&I(o,e,c).catch(d=>{console.error("Error in external state side effect, Please do it within side effects:",d)})},m=()=>{const[i,c]=r.useState(e);return r.useEffect(()=>(l.push(c),()=>{const u=l.indexOf(c);u>-1&&l.splice(u,1)}),[]),[a?.get?a.get(i):i,f]};return{get:s,set:f,use:m,useGetter:()=>{const[i]=m();return i},__listeners:l}}function le(t,n){const e=n||new Date,l=e.getFullYear(),o=e.getMonth()+1,a=e.getDate(),s=e.getHours(),f=e.getMinutes(),m=e.getSeconds(),i=e.getMilliseconds(),c=e.getDay(),u=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],d=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],g=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],A=["January","February","March","April","May","June","July","August","September","October","November","December"],P=d[c],D=u[c],T=o-1,Y=A[T],k=g[T],O={YY:l.toString().slice(2),YYYY:l.toString(),M:o.toString(),MM:o.toString().padStart(2,"0"),MMM:k,MMMM:Y,D:a.toString(),DD:a.toString().padStart(2,"0"),d:c.toString(),dd:D,ddd:D,dddd:P,H:s.toString(),HH:s.toString().padStart(2,"0"),h:(s%12).toString(),hh:(s%12).toString().padStart(2,"0"),m:f.toString(),mm:f.toString().padStart(2,"0"),s:m.toString(),ss:m.toString().padStart(2,"0"),SSS:i.toString().padStart(3,"0"),Z:"+08:00",ZZ:"+0800",A:s<12?"AM":"PM",a:s<12?"am":"pm"};return t.replace(/YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|Z{1,2}|A|a/g,j=>O[j])}class ae{count=0;next(){return this.count++}}export{Q as ArrayRender,ae as Counter,X as DateRender,Z as False,p as If,L as Pipe,U as Scope,G as SizeBox,b as Styles,y as Switch,K as Toggle,V as True,z as When,C as childrenLoop,re as createExternalState,M as cx,le as formatDate,I as safePromiseTry,te as useControlled};
1
+ import a,{useMemo as y,Children as _,Fragment as E,isValidElement as W,cloneElement as H,useEffect as I,useState as A,useRef as x,useCallback as B}from"react";function P(t,n){if(t===void 0)return;let e=0;if(Array.isArray(t)){for(const r of t)if(n(r,e++)===!1)break}else n(t,e)}const V=(t,n)=>t===n,w=t=>a.createElement(a.Fragment,null,t.children);w.displayName="Switch_Case";const N=t=>a.createElement(a.Fragment,null,t.children);N.displayName="Switch_Default";const g=t=>{const{value:n,compare:e=V,children:r,strict:o=!1}=t,l=new Set;let s=null,f=null,m=!1;return P(r,(c,i)=>{if(!a.isValidElement(c))throw new Error(`Switch Children only accepts valid React elements at index ${i}`);const u=c.type;if(u.displayName===w.displayName){const d=c.props;if(l.has(d.value))throw new Error(`Switch found duplicate Case value at index ${i}: ${JSON.stringify(d.value)}${o?" (detected in strict mode)":""}`);if(l.add(d.value),!s&&e(n,d.value)&&(s=d.children,o===!1))return!1}else if(u.displayName===N.displayName){if(m)throw new Error(`Switch can only have one Default child at index ${i}`);if(m=!0,f=c.props.children,!o&&s)return!1}else throw new Error(`Switch Children only accepts 'Case' or 'Default' elements, found: ${String(u.displayName||u.name||u)} at index ${i}`)}),a.createElement(a.Fragment,null,s??f)};g.displayName="Switch",g.Case=w,g.Default=N,g.createTyped=function(){return{Switch:g,Case:w,Default:N}};const v=t=>a.createElement(a.Fragment,null,t.children),b=({children:t})=>a.createElement(a.Fragment,null,t),M=t=>a.createElement(a.Fragment,null,t.children);v.displayName="If_Then",b.displayName="If_Else",M.displayName="If_ElseIf";const p=({condition:t,children:n})=>{let e=null,r=null;const o=[];if(a.Children.forEach(n,l=>{if(!a.isValidElement(l))throw new Error("If component only accepts valid React elements");const s=l.type;if(s.displayName===v.displayName){if(e)throw new Error("If component can only have one Then child");e=l}else if(s.displayName===M.displayName)o.push(l);else if(s.displayName===b.displayName){if(r)throw new Error("If component can only have one Else child");r=l}else throw new Error(`If component only accepts 'Then', 'ElseIf', or 'Else' elements as children, found: ${String(s.displayName||s.name||s)}`)}),t)return e?a.createElement(a.Fragment,null,e.props.children):null;for(const l of o)if(l.props.condition)return a.createElement(a.Fragment,null,l.props.children);return r?a.createElement(a.Fragment,null,r.props.children):null};p.displayName="If",p.Then=v,p.ElseIf=M,p.Else=b,p.createTyped=function(){return{If:p,Then:v,ElseIf:M,Else:b}};const Z=({condition:t,children:n})=>t?a.createElement(a.Fragment,null,n):null,z=({condition:t,children:n})=>t===!1?a.createElement(a.Fragment,null,n):null,L=({all:t,any:n,none:e,children:r,fallback:o})=>y(()=>(t&&(n||e)&&console.warn('When: Multiple condition types (all, any, none) provided; "all" takes precedence.'),!!(t&&t.length>0&&t.every(Boolean)||n&&n.length>0&&n.some(Boolean)||e&&e.length>0&&e.every(l=>!l))),[t,n,e])?a.createElement(a.Fragment,null,r):a.createElement(a.Fragment,null,o||null),G=({data:t,transform:n,render:e,fallback:r})=>{const o=y(()=>n.reduce((l,s)=>s(l),t),[t,n]);return o==null?a.createElement(a.Fragment,null,r||null):a.createElement(a.Fragment,null,e(o))},U=t=>{const{children:n,h:e,w:r,size:o,height:l,width:s,className:f}=t;return a.createElement("div",{style:{width:o||r||s,height:o||e||l,flexShrink:0},className:f},n)},q=({let:t,props:n,children:e,fallback:r})=>{const o=y(()=>typeof t=="function"?t(n):t,[t,n]);return!e||!Object.keys(o).length?a.createElement(a.Fragment,null,r||null):a.createElement(a.Fragment,null,e(o))};function F(...t){const n=new Set;for(const e of t)if(e){if(typeof e=="string")n.add(e);else if(Array.isArray(e))e.forEach(r=>n.add(r));else if(typeof e=="object")for(const[r,o]of Object.entries(e))o&&n.add(r)}return Array.from(n).join(" ")}const K=t=>typeof t=="object"&&!!t,T=({className:t,children:n,asWrapper:e=!1})=>{if(!n)return null;if(_.count(n)>1)return console.error("<Styles>: children has more than one child. Please check your code."),a.createElement(E,null,n);if(!t)return a.createElement(E,null,n);const r=typeof t=="string"?t:F(...Object.values(t));if(e)return a.createElement(e===!0?"div":e,{className:r},n);if(W(n)){const o=n;let l=o?.props?.className;return o?.type?.displayName===T.displayName&&K(l)&&(l=F(...Object.values(l))),H(n,{className:F(r,l)})}return console.error("<Styles>: children is not a valid React element. Please check your code."),a.createElement(E,null,n)};T.displayName="W/Styles";const Q=t=>{const{index:n=0,options:e,next:r,render:o}=t;I(()=>{if(e.length<n+1)throw new Error(`Index ${n} is out of bounds for options array of length ${e.length}. Defaulting to first option.`)},[n,e]);const[l,s]=A(n),f=()=>{s(m=>e.length?r?r(m,e):(m+1)%e.length:m)};return o(e[l],f)},X=({onIntersect:t,threshold:n=.1,root:e=null,rootMargin:r="0px",triggerOnce:o=!1,disabled:l=!1,children:s,className:f,style:m})=>{const c=x(null),i=x(null),u=x(!1);return I(()=>{if(l||!c.current)return;if(!window.IntersectionObserver){console.warn("IntersectionObserver is not supported in this browser");return}const d=c.current,h=D=>{D.forEach(S=>{if(S.isIntersecting){if(o&&u.current)return;t(S,i.current),o&&(u.current=!0,i.current?.unobserve(d))}})};return i.current=new IntersectionObserver(h,{root:e,rootMargin:r,threshold:n}),i.current.observe(d),()=>{i.current&&i.current.disconnect()}},[t,n,e,r,o,l]),I(()=>{o||(u.current=!1)},[o]),a.createElement("div",{ref:c,className:f,style:m},s)};function $(t){const{items:n,renderItem:e,filter:r}=t;return n?a.createElement(E,null,n.map((o,l)=>r&&!r(o)?null:e(o,l))):(console.error("ArrayRender: items is null"),null)}function ee({source:t,format:n,children:e}){const r=y(()=>{if(t instanceof Date)return t;if(typeof t=="string"||typeof t=="number"){const l=new Date(t);return isNaN(l.getTime())?null:l}return null},[t]),o=y(()=>r?n?n(r):r.toLocaleString():null,[r,n]);return!o||!e?null:a.createElement(a.Fragment,null,e(o))}const te="onChange",ne="value";function re(t){const{defaultValue:n,onBeforeChange:e,trigger:r=te,valuePropName:o=ne,props:l}=t,s=Object.prototype.hasOwnProperty.call(l,o),[f,m]=A(n),c=s?l[o]:f,i=y(()=>l[r],[l,r]),u=B(d=>{const h=typeof d=="function"?d(c):d;e&&e(h,c)===!1||(s||m(h),i&&i(h))},[s,e,c,i]);return[c,u]}function le(t,...n){try{const e=t(...n);return e instanceof Promise?e:Promise.resolve(e)}catch(e){return Promise.reject(e)}}const Y=typeof Promise.try=="function"?Promise.try.bind(Promise):le;function ae(t,n={}){let e=typeof t=="function"?t():t;const r=[],{sideEffect:o,transform:l}=n,s=()=>{const c=e;return l?.get?l.get(c):c},f=c=>{const i=e,u=l?.get?l.get(i):i;e=l?.set?l.set(typeof c=="function"?c(u):c):typeof c=="function"?c(u):c,r.forEach(d=>d(e)),o&&Y(o,e,i).catch(d=>{console.error("Error in external state side effect, Please do it within side effects:",d)})},m=()=>{const[c,i]=a.useState(e);return a.useEffect(()=>(r.push(i),()=>{const u=r.indexOf(i);u>-1&&r.splice(u,1)}),[]),[l?.get?l.get(c):c,f]};return{get:s,set:f,use:m,useGetter:()=>{const[c]=m();return c},__listeners:r}}function oe(t,n){const e=n||new Date,r=e.getFullYear(),o=e.getMonth()+1,l=e.getDate(),s=e.getHours(),f=e.getMinutes(),m=e.getSeconds(),c=e.getMilliseconds(),i=e.getDay(),u=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],d=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],h=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],D=["January","February","March","April","May","June","July","August","September","October","November","December"],S=d[i],C=u[i],O=o-1,k=D[O],j=h[O],J={YY:r.toString().slice(2),YYYY:r.toString(),M:o.toString(),MM:o.toString().padStart(2,"0"),MMM:j,MMMM:k,D:l.toString(),DD:l.toString().padStart(2,"0"),d:i.toString(),dd:C,ddd:C,dddd:S,H:s.toString(),HH:s.toString().padStart(2,"0"),h:(s%12).toString(),hh:(s%12).toString().padStart(2,"0"),m:f.toString(),mm:f.toString().padStart(2,"0"),s:m.toString(),ss:m.toString().padStart(2,"0"),SSS:c.toString().padStart(3,"0"),Z:"+08:00",ZZ:"+0800",A:s<12?"AM":"PM",a:s<12?"am":"pm"};return t.replace(/YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|Z{1,2}|A|a/g,R=>J[R])}class se{count=0;next(){return this.count++}}export{$ as ArrayRender,se as Counter,ee as DateRender,z as False,p as If,X as Observer,G as Pipe,q as Scope,U as SizeBox,T as Styles,g as Switch,Q as Toggle,Z as True,L as When,P as childrenLoop,ae as createExternalState,F as cx,oe as formatDate,Y as safePromiseTry,re as useControlled};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wwog/react",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "A practical React component library providing declarative flow control and common UI utility components to make your React code more concise and readable.",
5
5
  "keywords": [
6
6
  "react",
@@ -20,15 +20,6 @@
20
20
  ],
21
21
  "homepage": "https://github.com/wwog/react",
22
22
  "license": "MIT",
23
- "scripts": {
24
- "build": "unbuild",
25
- "format": "biome format --write src",
26
- "check": "biome check --write src",
27
- "test:unit": "vitest run --browser.headless",
28
- "test:types": "tsc --noEmit --skipLibCheck",
29
- "all-suites": "pnpm run format && pnpm run check && pnpm run test:types && pnpm run test:unit",
30
- "test:watch": "vitest"
31
- },
32
23
  "devDependencies": {
33
24
  "@biomejs/biome": "^1.9.4",
34
25
  "@types/react": "^19.1.2",
@@ -50,5 +41,13 @@
50
41
  "node": ">= 20.0.0",
51
42
  "pnpm": ">=8.15.0"
52
43
  },
53
- "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
54
- }
44
+ "scripts": {
45
+ "build": "unbuild",
46
+ "format": "biome format --write src",
47
+ "check": "biome check --write src",
48
+ "test:unit": "vitest run --browser.headless",
49
+ "test:types": "tsc --noEmit --skipLibCheck",
50
+ "all-suites": "pnpm run format && pnpm run check && pnpm run test:types && pnpm run test:unit",
51
+ "test:watch": "vitest"
52
+ }
53
+ }
@@ -0,0 +1,168 @@
1
+ import React, { useEffect, useRef, type ReactNode } from "react";
2
+
3
+ export interface ObserverProps {
4
+ /**
5
+ * @description_en Callback function when intersection occurs.
6
+ * @description_zh 交叉时触发的回调函数。
7
+ */
8
+ onIntersect: (entry: IntersectionObserverEntry, observer: IntersectionObserver) => void;
9
+ /**
10
+ * @description_en Threshold(s) at which to trigger the callback. Default is 0.1.
11
+ * @description_zh 触发回调的阈值,默认为 0.1。
12
+ * @optional
13
+ * @default 0.1
14
+ */
15
+ threshold?: number | number[];
16
+ /**
17
+ * @description_en The root element for intersection. Default is viewport.
18
+ * @description_zh 交叉的根元素,默认为视口。
19
+ * @optional
20
+ * @default null
21
+ */
22
+ root?: Element | Document | null;
23
+ /**
24
+ * @description_en Margin around the root. Default is "0px".
25
+ * @description_zh 根元素周围的边距,默认为 "0px"。
26
+ * @optional
27
+ * @default "0px"
28
+ */
29
+ rootMargin?: string;
30
+ /**
31
+ * @description_en Whether to trigger only once. Default is false.
32
+ * @description_zh 是否只触发一次,默认为 false。
33
+ * @optional
34
+ * @default false
35
+ */
36
+ triggerOnce?: boolean;
37
+ /**
38
+ * @description_en Whether to disable the observer. Default is false.
39
+ * @description_zh 是否禁用观察者,默认为 false。
40
+ * @optional
41
+ * @default false
42
+ */
43
+ disabled?: boolean;
44
+ /**
45
+ * @description_en Child elements to observe.
46
+ * @description_zh 要观察的子元素。
47
+ * @optional
48
+ */
49
+ children?: ReactNode;
50
+ /**
51
+ * @description_en CSS class name.
52
+ * @description_zh CSS 类名。
53
+ * @optional
54
+ */
55
+ className?: string;
56
+ /**
57
+ * @description_en Inline styles.
58
+ * @description_zh 内联样式。
59
+ * @optional
60
+ */
61
+ style?: React.CSSProperties;
62
+ }
63
+
64
+ /*
65
+ 提供声明式的观察者API封装。
66
+ 简化懒加载、无限滚动等常见场景的实现。
67
+ 支持灵活的配置选项和一次性触发。
68
+ */
69
+ /**
70
+ * @description_zh 交叉观察者组件,用于监听元素与视口的交叉状态,常用于懒加载和无限滚动场景。
71
+ * @description_en Intersection Observer component for monitoring element-viewport intersection, commonly used for lazy loading and infinite scrolling.
72
+ * @component
73
+ * @example
74
+ * ```tsx
75
+ * // 懒加载示例
76
+ * <Observer onIntersect={loadImage} triggerOnce>
77
+ * <img data-src="image.jpg" alt="Lazy loaded" />
78
+ * </Observer>
79
+ *
80
+ * // 无限滚动示例
81
+ * <Observer onIntersect={loadMore} threshold={0.1}>
82
+ * <div>滚动到这里加载更多</div>
83
+ * </Observer>
84
+ *
85
+ * // 自定义根元素和边距
86
+ * <Observer
87
+ * onIntersect={handleIntersect}
88
+ * root={scrollContainer}
89
+ * rootMargin="100px"
90
+ * threshold={[0, 0.5, 1]}
91
+ * >
92
+ * <div>观察目标</div>
93
+ * </Observer>
94
+ * ```
95
+ */
96
+ export const Observer: React.FC<ObserverProps> = ({
97
+ onIntersect,
98
+ threshold = 0.1,
99
+ root = null,
100
+ rootMargin = "0px",
101
+ triggerOnce = false,
102
+ disabled = false,
103
+ children,
104
+ className,
105
+ style,
106
+ }) => {
107
+ const elementRef = useRef<HTMLDivElement>(null);
108
+ const observerRef = useRef<IntersectionObserver | null>(null);
109
+ const hasTriggeredRef = useRef(false);
110
+
111
+ useEffect(() => {
112
+ if (disabled || !elementRef.current) {
113
+ return;
114
+ }
115
+
116
+ // 检查浏览器支持
117
+ if (!window.IntersectionObserver) {
118
+ console.warn('IntersectionObserver is not supported in this browser');
119
+ return;
120
+ }
121
+
122
+ const element = elementRef.current;
123
+
124
+ const handleIntersect = (entries: IntersectionObserverEntry[]) => {
125
+ entries.forEach((entry) => {
126
+ if (entry.isIntersecting) {
127
+ if (triggerOnce && hasTriggeredRef.current) {
128
+ return;
129
+ }
130
+
131
+ onIntersect(entry, observerRef.current!);
132
+
133
+ if (triggerOnce) {
134
+ hasTriggeredRef.current = true;
135
+ observerRef.current?.unobserve(element);
136
+ }
137
+ }
138
+ });
139
+ };
140
+
141
+ observerRef.current = new IntersectionObserver(handleIntersect, {
142
+ root,
143
+ rootMargin,
144
+ threshold,
145
+ });
146
+
147
+ observerRef.current.observe(element);
148
+
149
+ return () => {
150
+ if (observerRef.current) {
151
+ observerRef.current.disconnect();
152
+ }
153
+ };
154
+ }, [onIntersect, threshold, root, rootMargin, triggerOnce, disabled]);
155
+
156
+ // 重置触发状态(当 triggerOnce 从 true 变为 false 时)
157
+ useEffect(() => {
158
+ if (!triggerOnce) {
159
+ hasTriggeredRef.current = false;
160
+ }
161
+ }, [triggerOnce]);
162
+
163
+ return (
164
+ <div ref={elementRef} className={className} style={style}>
165
+ {children}
166
+ </div>
167
+ );
168
+ };
@@ -2,3 +2,4 @@ export * from "./SizeBox";
2
2
  export * from "./Scope";
3
3
  export * from "./Styles";
4
4
  export * from "./Toggle";
5
+ export * from "./Observer";