@wwog/react 1.2.21 → 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
@@ -189,7 +189,9 @@ function UserList({ users }) {
189
189
 
190
190
  #### `<Clamp>` (v1.2.14+)
191
191
 
192
- A component for displaying text with a fixed number of lines, ellipsis, and optional extra content. Highly compatible without using webkit-box or JavaScript tricks.
192
+ > Removed in v1.3.0. The compatibility problem is too big, the desktop web page works well, h5 has a problem.
193
+
194
+ A component for displaying text with a fixed number of lines, ellipsis, and optional extra content.
193
195
 
194
196
  ```tsx
195
197
  import { Clamp } from "@wwog/react";
@@ -311,6 +313,69 @@ function Example() {
311
313
  - `format`: Optional function to format the date, defaults to `toLocaleString()`.
312
314
  - `children`: Function to render the formatted date, receives the formatted date as an argument.
313
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
+
314
379
  #### `<SizeBox>`
315
380
 
316
381
  Create a fixed-size container for layout adjustment and spacing control.
@@ -435,6 +500,7 @@ function ReadOnlyThemeConsumer() {
435
500
  ```
436
501
 
437
502
  - `createExternalState<T>(initialState, options?)`: Creates a state accessible outside components
503
+
438
504
  - `initialState`: Initial state value
439
505
  - `options.sideEffect`: Optional side effect function, called on state updates
440
506
  - Returns an object with methods:
@@ -442,10 +508,8 @@ function ReadOnlyThemeConsumer() {
442
508
  - `set(newState)`: Update the state value
443
509
  - `use()`: React Hook, returns `[state, setState]` for using this state in components
444
510
  - `useGetter()`: React Hook that only returns the state value, useful when you only need to read the state
445
- - `options.transform`:
446
- - `get`
447
- - `set`
448
- Use cases:
511
+ - `options.transform`: - `get` - `set`
512
+ Use cases:
449
513
 
450
514
  - Global state management (themes, user settings, etc.)
451
515
  - Cross-component communication
package/dist/index.d.mts CHANGED
@@ -372,34 +372,94 @@ interface ToggleProps<T = boolean> {
372
372
  */
373
373
  declare const Toggle: <T>(props: ToggleProps<T>) => React$1.ReactNode;
374
374
 
375
- interface ClampProps {
375
+ interface ObserverProps {
376
376
  /**
377
- * @description 最大行数
378
- * @description_en maximum number of lines
379
- * @default 1
377
+ * @description_en Callback function when intersection occurs.
378
+ * @description_zh 交叉时触发的回调函数。
380
379
  */
381
- maxLine?: number;
382
- extraContent?: React$1.ReactNode;
380
+ onIntersect: (entry: IntersectionObserverEntry, observer: IntersectionObserver) => void;
383
381
  /**
384
- * @description 用于控制额外内容的高度,如果出现没有正常显示请调节此属性
385
- * @description_en used to control the height of the extra content. If it does not display normally, please adjust this property
386
- * @default 20
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
387
426
  */
388
- extraHeight?: number;
427
+ className?: string;
389
428
  /**
390
- * @description 显示的文本
391
- * @description_en text to be displayed
429
+ * @description_en Inline styles.
430
+ * @description_zh 内联样式。
431
+ * @optional
392
432
  */
393
- text: string;
394
- wrapperStyle?: React$1.CSSProperties;
433
+ style?: React$1.CSSProperties;
395
434
  }
396
435
  /**
397
- * @description 用于固定行数,显示省略号且显示额外内容的组件。兼容性非常好,没有用到webkit-box和js。
398
- * @description_en used to fix the number of lines, display ellipsis and display extra content. The compatibility is very good, without using webkit-box and js.
399
- * @param props
400
- * @returns
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
+ * ```
401
461
  */
402
- declare const Clamp: FC<ClampProps>;
462
+ declare const Observer: React$1.FC<ObserverProps>;
403
463
 
404
464
  interface ArrayRenderProps<T> {
405
465
  items: T[];
@@ -642,5 +702,5 @@ declare class Counter {
642
702
 
643
703
  declare const safePromiseTry: <T, U extends unknown[]>(callbackFn: (...args: U) => T | PromiseLike<T>, ...args: U) => Promise<Awaited<T>>;
644
704
 
645
- export { ArrayRender, Clamp, Counter, DateRender, False, If, Pipe, Scope, SizeBox, Styles, Switch, Toggle, True, When, childrenLoop, createExternalState, cx, formatDate, safePromiseTry, useControlled };
646
- export type { ArrayRenderProps, ClampProps, 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 y,Children as B,Fragment as w,isValidElement as W,cloneElement as J,useEffect as R,useState as I,useRef as _,useLayoutEffect as L,useCallback as Z}from"react";function P(t,n){if(t===void 0)return;let e=0;if(Array.isArray(t)){for(const a of t)if(n(a,e++)===!1)break}else n(t,e)}const z=(t,n)=>t===n,v=t=>r.createElement(r.Fragment,null,t.children);v.displayName="Switch_Case";const N=t=>r.createElement(r.Fragment,null,t.children);N.displayName="Switch_Default";const S=t=>{const{value:n,compare:e=z,children:a,strict:o=!1}=t,l=new Set;let i=null,f=null,m=!1;return P(a,(s,u)=>{if(!r.isValidElement(s))throw new Error(`Switch Children only accepts valid React elements at index ${u}`);const c=s.type;if(c.displayName===v.displayName){const d=s.props;if(l.has(d.value))throw new Error(`Switch found duplicate Case value at index ${u}: ${JSON.stringify(d.value)}${o?" (detected in strict mode)":""}`);if(l.add(d.value),!i&&e(n,d.value)&&(i=d.children,o===!1))return!1}else if(c.displayName===N.displayName){if(m)throw new Error(`Switch can only have one Default child at index ${u}`);if(m=!0,f=s.props.children,!o&&i)return!1}else throw new Error(`Switch Children only accepts 'Case' or 'Default' elements, found: ${String(c.displayName||c.name||c)} at index ${u}`)}),r.createElement(r.Fragment,null,i??f)};S.displayName="Switch",S.Case=v,S.Default=N,S.createTyped=function(){return{Switch:S,Case:v,Default:N}};const b=t=>r.createElement(r.Fragment,null,t.children),M=({children:t})=>r.createElement(r.Fragment,null,t),F=t=>r.createElement(r.Fragment,null,t.children);b.displayName="If_Then",M.displayName="If_Else",F.displayName="If_ElseIf";const g=({condition:t,children:n})=>{let e=null,a=null;const o=[];if(r.Children.forEach(n,l=>{if(!r.isValidElement(l))throw new Error("If component only accepts valid React elements");const i=l.type;if(i.displayName===b.displayName){if(e)throw new Error("If component can only have one Then child");e=l}else if(i.displayName===F.displayName)o.push(l);else if(i.displayName===M.displayName){if(a)throw new Error("If component can only have one Else child");a=l}else throw new Error(`If component only accepts 'Then', 'ElseIf', or 'Else' elements as children, found: ${String(i.displayName||i.name||i)}`)}),t)return e?r.createElement(r.Fragment,null,e.props.children):null;for(const l of o)if(l.props.condition)return r.createElement(r.Fragment,null,l.props.children);return a?r.createElement(r.Fragment,null,a.props.children):null};g.displayName="If",g.Then=b,g.ElseIf=F,g.Else=M,g.createTyped=function(){return{If:g,Then:b,ElseIf:F,Else:M}};const T=({condition:t,children:n})=>t?r.createElement(r.Fragment,null,n):null,V=({condition:t,children:n})=>t===!1?r.createElement(r.Fragment,null,n):null,G=({all:t,any:n,none:e,children:a,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])?r.createElement(r.Fragment,null,a):r.createElement(r.Fragment,null,o||null),U=({data:t,transform:n,render:e,fallback:a})=>{const o=y(()=>n.reduce((l,i)=>i(l),t),[t,n]);return o==null?r.createElement(r.Fragment,null,a||null):r.createElement(r.Fragment,null,e(o))},q=t=>{const{children:n,h:e,w:a,size:o,height:l,width:i,className:f}=t;return r.createElement("div",{style:{width:o||a||i,height:o||e||l,flexShrink:0},className:f},n)},K=({let:t,props:n,children:e,fallback:a})=>{const o=y(()=>typeof t=="function"?t(n):t,[t,n]);return!e||!Object.keys(o).length?r.createElement(r.Fragment,null,a||null):r.createElement(r.Fragment,null,e(o))};function x(...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(a=>n.add(a));else if(typeof e=="object")for(const[a,o]of Object.entries(e))o&&n.add(a)}return Array.from(n).join(" ")}const Q=t=>typeof t=="object"&&!!t,A=({className:t,children:n,asWrapper:e=!1})=>{if(!n)return null;if(B.count(n)>1)return console.error("<Styles>: children has more than one child. Please check your code."),r.createElement(w,null,n);if(!t)return r.createElement(w,null,n);const a=typeof t=="string"?t:x(...Object.values(t));if(e)return r.createElement(e===!0?"div":e,{className:a},n);if(W(n)){const o=n;let l=o?.props?.className;return o?.type?.displayName===A.displayName&&Q(l)&&(l=x(...Object.values(l))),J(n,{className:x(a,l)})}return console.error("<Styles>: children is not a valid React element. Please check your code."),r.createElement(w,null,n)};A.displayName="W/Styles";const X=t=>{const{index:n=0,options:e,next:a,render:o}=t;R(()=>{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,i]=I(n),f=()=>{i(m=>e.length?a?a(m,e):(m+1)%e.length:m)};return o(e[l],f)},$=t=>{const{maxLine:n=1,text:e,extraHeight:a=22,extraContent:o,wrapperStyle:l}=t,i=_(null),[f,m]=I(!1),s=y(()=>!(e==null||e===""),[e]),u=()=>{if(!i.current)return;const c=document.createElement("div");c.textContent=e;const d=getComputedStyle(i.current);if(c.style.width=d.width,c.style.fontSize=d.fontSize,c.style.lineHeight=d.lineHeight,c.style.wordBreak=d.wordBreak,c.style.visibility="hidden",l){const E=Object.keys(l).map(p=>p.replace(/[A-Z]/g,k=>`-${k.toLowerCase()}`));for(const p of E)c.style[p]=d[p]}document.body.appendChild(c);const h=parseInt(getComputedStyle(c).lineHeight)||20,C=c.offsetHeight,D=Math.round(C/h);document.body.removeChild(c),m(D>n)};return L(()=>{s&&u()},[n,e,s]),r.createElement(T,{condition:s},r.createElement("div",{ref:i,style:{overflow:"hidden",width:"100%",display:"flex",...l}},r.createElement("div",{style:{display:"-webkit-box",WebkitBoxOrient:"vertical",WebkitLineClamp:n,overflow:"hidden",wordBreak:"break-all"}},r.createElement(T,{condition:f},r.createElement("div",{style:{float:"right",height:"100%",marginBottom:-a}}),r.createElement("div",{style:{float:"right",clear:"both",height:a}},o)),e)))};function ee(t){const{items:n,renderItem:e,filter:a}=t;return n?r.createElement(w,null,n.map((o,l)=>a&&!a(o)?null:e(o,l))):(console.error("ArrayRender: items is null"),null)}function te({source:t,format:n,children:e}){const a=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(()=>a?n?n(a):a.toLocaleString():null,[a,n]);return!o||!e?null:r.createElement(r.Fragment,null,e(o))}const ne="onChange",re="value";function le(t){const{defaultValue:n,onBeforeChange:e,trigger:a=ne,valuePropName:o=re,props:l}=t,i=Object.prototype.hasOwnProperty.call(l,o),[f,m]=I(n),s=i?l[o]:f,u=y(()=>l[a],[l,a]),c=Z(d=>{const h=typeof d=="function"?d(s):d;e&&e(h,s)===!1||(i||m(h),u&&u(h))},[i,e,s,u]);return[s,c]}function ae(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):ae;function oe(t,n={}){let e=typeof t=="function"?t():t;const a=[],{sideEffect:o,transform:l}=n,i=()=>{const s=e;return l?.get?l.get(s):s},f=s=>{const u=e,c=l?.get?l.get(u):u;e=l?.set?l.set(typeof s=="function"?s(c):s):typeof s=="function"?s(c):s,a.forEach(d=>d(e)),o&&Y(o,e,u).catch(d=>{console.error("Error in external state side effect, Please do it within side effects:",d)})},m=()=>{const[s,u]=r.useState(e);return r.useEffect(()=>(a.push(u),()=>{const c=a.indexOf(u);c>-1&&a.splice(c,1)}),[]),[l?.get?l.get(s):s,f]};return{get:i,set:f,use:m,useGetter:()=>{const[s]=m();return s},__listeners:a}}function ie(t,n){const e=n||new Date,a=e.getFullYear(),o=e.getMonth()+1,l=e.getDate(),i=e.getHours(),f=e.getMinutes(),m=e.getSeconds(),s=e.getMilliseconds(),u=e.getDay(),c=["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"],C=["January","February","March","April","May","June","July","August","September","October","November","December"],D=d[u],E=c[u],p=o-1,k=C[p],O=h[p],j={YY:a.toString().slice(2),YYYY:a.toString(),M:o.toString(),MM:o.toString().padStart(2,"0"),MMM:O,MMMM:k,D:l.toString(),DD:l.toString().padStart(2,"0"),d:u.toString(),dd:E,ddd:E,dddd:D,H:i.toString(),HH:i.toString().padStart(2,"0"),h:(i%12).toString(),hh:(i%12).toString().padStart(2,"0"),m:f.toString(),mm:f.toString().padStart(2,"0"),s:m.toString(),ss:m.toString().padStart(2,"0"),SSS:s.toString().padStart(3,"0"),Z:"+08:00",ZZ:"+0800",A:i<12?"AM":"PM",a:i<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,H=>j[H])}class se{count=0;next(){return this.count++}}export{ee as ArrayRender,$ as Clamp,se as Counter,te as DateRender,V as False,g as If,U as Pipe,K as Scope,q as SizeBox,A as Styles,S as Switch,X as Toggle,T as True,G as When,P as childrenLoop,oe as createExternalState,x as cx,ie as formatDate,Y as safePromiseTry,le 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.2.21",
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,4 +2,4 @@ export * from "./SizeBox";
2
2
  export * from "./Scope";
3
3
  export * from "./Styles";
4
4
  export * from "./Toggle";
5
- export * from "./Clamp";
5
+ export * from "./Observer";
File without changes