lupine.components 1.1.9 → 1.1.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lupine.components",
3
- "version": "1.1.9",
3
+ "version": "1.1.11",
4
4
  "license": "MIT",
5
5
  "author": "uuware.com",
6
6
  "homepage": "https://github.com/uuware/lupine.js",
@@ -39,4 +39,4 @@
39
39
  "dependencies": {
40
40
  "lupine.web": "^1.0.0"
41
41
  }
42
- }
42
+ }
@@ -1,14 +1,16 @@
1
1
  import { CssProps, RefProps, VNode, mountInnerComponent } from 'lupine.web';
2
2
  import { backActionHelper } from '../lib';
3
3
 
4
- export type ActionSheetCloseProps = () => void;
4
+ export type ActionSheetCloseProps = (reason?: ActionSheetCloseReasonProps) => void;
5
+
6
+ export type ActionSheetCloseReasonProps = 'cancel' | 'confirm' | 'select' | undefined;
5
7
 
6
8
  export type ActionSheetShowProps = {
7
9
  title: string;
8
10
  children: string | VNode<any>;
9
11
  contentMaxWidth?: string;
10
12
  contentMaxHeight?: string;
11
- closeEvent?: () => void;
13
+ closeEvent?: (reason?: ActionSheetCloseReasonProps) => void;
12
14
  closeWhenClickOutside?: boolean; // default true
13
15
  confirmButtonText?: string; // no showing if not set
14
16
  handleConfirmClicked?: (close: ActionSheetCloseProps) => void;
@@ -36,19 +38,19 @@ export class ActionSheet {
36
38
  if (handleConfirmClicked) {
37
39
  handleConfirmClicked(handleClose);
38
40
  } else {
39
- handleClose();
41
+ handleClose('confirm');
40
42
  }
41
43
  };
42
44
  const onCancel = () => {
43
- handleClose();
45
+ handleClose('cancel');
44
46
  };
45
47
  const onClickContainer = (event: any) => {
46
48
  if (closeWhenClickOutside !== false && event.target.classList.contains('act-sheet-box')) {
47
- handleClose();
49
+ handleClose('cancel');
48
50
  }
49
51
  };
50
- const handleClose = () => {
51
- closeEvent?.();
52
+ const handleClose = (reason?: ActionSheetCloseReasonProps) => {
53
+ closeEvent?.(reason);
52
54
  ref.current.classList.remove('animation-open');
53
55
  setTimeout(() => {
54
56
  base.remove();
@@ -208,6 +210,7 @@ export class ActionSheetMessage {
208
210
  static async show({
209
211
  title,
210
212
  message,
213
+ contentMaxWidth,
211
214
  contentMaxHeight,
212
215
  closeWhenClickOutside = true,
213
216
  confirmButtonText,
@@ -216,7 +219,15 @@ export class ActionSheetMessage {
216
219
  }: ActionSheetMessageProps): Promise<ActionSheetCloseProps> {
217
220
  const handleClose = await ActionSheet.show({
218
221
  title,
219
- children: <div css={{ padding: '8px', borderTop: '1px solid var(--primary-border-color)' }}>{message}</div>,
222
+ children: (
223
+ <div
224
+ css={{ padding: '8px', borderTop: '1px solid var(--primary-border-color)' }}
225
+ onClick={() => handleClose('select')}
226
+ >
227
+ {message}
228
+ </div>
229
+ ),
230
+ contentMaxWidth,
220
231
  contentMaxHeight,
221
232
  confirmButtonText,
222
233
  handleConfirmClicked,
@@ -227,6 +238,41 @@ export class ActionSheetMessage {
227
238
  }
228
239
  }
229
240
 
241
+ export type ActionSheetMessagePromiseProps = {
242
+ message: string | VNode<any>;
243
+ title: string;
244
+ contentMaxWidth?: string;
245
+ contentMaxHeight?: string;
246
+ closeWhenClickOutside?: boolean;
247
+ confirmButtonText?: string;
248
+ zIndex?: string;
249
+ };
250
+ export const ActionSheetMessagePromise = async ({
251
+ title,
252
+ message,
253
+ contentMaxWidth,
254
+ contentMaxHeight,
255
+ closeWhenClickOutside = true,
256
+ confirmButtonText,
257
+ zIndex,
258
+ }: ActionSheetMessagePromiseProps): Promise<void> => {
259
+ return new Promise(async (resolve, reject) => {
260
+ const closeEvent = (reason?: ActionSheetCloseReasonProps) => {
261
+ resolve();
262
+ };
263
+ await ActionSheet.show({
264
+ title,
265
+ children: <div css={{ padding: '8px', borderTop: '1px solid var(--primary-border-color)' }}>{message}</div>,
266
+ contentMaxWidth,
267
+ contentMaxHeight,
268
+ confirmButtonText,
269
+ closeEvent,
270
+ closeWhenClickOutside,
271
+ zIndex,
272
+ });
273
+ });
274
+ };
275
+
230
276
  export type ActionSheetInputProps = Omit<
231
277
  ActionSheetShowProps,
232
278
  'children' | 'handleClicked' | 'closeEvent' | 'handleConfirmClicked'
@@ -268,3 +314,106 @@ export class ActionSheetInput {
268
314
  return handleClose;
269
315
  }
270
316
  }
317
+
318
+ export type ActionSheetInputPromiseProps = {
319
+ defaultValue?: string;
320
+ title: string;
321
+ contentMaxWidth?: string;
322
+ contentMaxHeight?: string;
323
+ closeWhenClickOutside?: boolean;
324
+ confirmButtonText?: string;
325
+ cancelButtonText?: string;
326
+ zIndex?: string;
327
+ };
328
+ export const ActionSheetInputPromise = async ({
329
+ title,
330
+ defaultValue,
331
+ contentMaxWidth,
332
+ contentMaxHeight,
333
+ closeWhenClickOutside = true,
334
+ confirmButtonText = 'OK',
335
+ cancelButtonText = 'Cancel',
336
+ zIndex,
337
+ }: ActionSheetInputPromiseProps): Promise<string | undefined> => {
338
+ return new Promise(async (resolve, reject) => {
339
+ const closeEvent = (reason?: ActionSheetCloseReasonProps) => {
340
+ if (reason !== 'confirm') {
341
+ resolve(undefined);
342
+ }
343
+ };
344
+ let value: string = defaultValue || '';
345
+ await ActionSheet.show({
346
+ title,
347
+ children: (
348
+ <div css={{ padding: '8px', borderTop: '1px solid var(--primary-border-color)' }}>
349
+ <input
350
+ class='input-base w-100p'
351
+ type='text'
352
+ value={value}
353
+ onInput={(e) => (value = (e.target as HTMLInputElement).value)}
354
+ />
355
+ </div>
356
+ ),
357
+ contentMaxWidth,
358
+ contentMaxHeight,
359
+ confirmButtonText,
360
+ handleConfirmClicked: (close) => {
361
+ resolve(value);
362
+ close('confirm');
363
+ },
364
+ closeEvent,
365
+ cancelButtonText,
366
+ closeWhenClickOutside,
367
+ zIndex,
368
+ });
369
+ });
370
+ };
371
+
372
+ export type ActionSheetSelectPromiseProps = {
373
+ title: string;
374
+ contentMaxWidth?: string;
375
+ contentMaxHeight?: string;
376
+ options?: string[];
377
+ closeWhenClickOutside?: boolean;
378
+ cancelButtonText?: string;
379
+ zIndex?: string;
380
+ };
381
+ export const ActionSheetSelectPromise = async ({
382
+ title,
383
+ contentMaxWidth,
384
+ contentMaxHeight,
385
+ options = ActionSheetSelectOptionsProps.Ok,
386
+ closeWhenClickOutside = true,
387
+ cancelButtonText = 'Cancel',
388
+ zIndex,
389
+ }: ActionSheetSelectPromiseProps): Promise<number> => {
390
+ return new Promise(async (resolve, reject) => {
391
+ const handleClicked = async (index: number, close: ActionSheetCloseProps) => {
392
+ resolve(index);
393
+ close('select');
394
+ };
395
+ const closeEvent = (reason?: ActionSheetCloseReasonProps) => {
396
+ if (reason !== 'select') {
397
+ resolve(-1);
398
+ }
399
+ };
400
+ const handleClose = await ActionSheet.show({
401
+ title,
402
+ children: (
403
+ <div>
404
+ {options.map((option, index) => (
405
+ <div class='act-sheet-item' key={index} onClick={() => handleClicked(index, handleClose)}>
406
+ {option}
407
+ </div>
408
+ ))}
409
+ </div>
410
+ ),
411
+ contentMaxWidth,
412
+ contentMaxHeight,
413
+ cancelButtonText,
414
+ closeEvent,
415
+ closeWhenClickOutside,
416
+ zIndex,
417
+ });
418
+ });
419
+ };
@@ -0,0 +1,41 @@
1
+ import { RefProps, VNode } from 'lupine.web';
2
+
3
+ // load async html
4
+ /*
5
+ <HtmlLoad
6
+ html={async () => {
7
+ return <Footer title={await WebConfig.get('footer', `XXX`)}></Footer>;
8
+ }}
9
+ ></HtmlLoad>
10
+ */
11
+ export type HtmlLoadHookProps = {
12
+ getRef?: () => RefProps;
13
+ render?: (html: string | VNode<any>) => void;
14
+ };
15
+ export type HtmlLoadProps = {
16
+ html: () => Promise<VNode<any>>;
17
+ initialHtml?: string | VNode<any>;
18
+ hook?: HtmlLoadHookProps;
19
+ };
20
+ export const HtmlLoad = (props: HtmlLoadProps) => {
21
+ const ref: RefProps = {
22
+ onLoad: async (el: Element) => {
23
+ const dom = await props.html();
24
+ await ref.mountInnerComponent!(dom);
25
+ },
26
+ };
27
+ if (props.hook) {
28
+ props.hook.getRef = () => ref;
29
+ props.hook.render = (html: string | VNode<any>) => {
30
+ ref.mountInnerComponent!(html);
31
+ };
32
+ }
33
+ return {
34
+ type: 'Fragment',
35
+ props: {
36
+ ref: ref,
37
+ children: props.initialHtml || '',
38
+ },
39
+ html: [],
40
+ };
41
+ };
@@ -1,15 +1,15 @@
1
1
  import { RefProps, VNode } from 'lupine.web';
2
2
 
3
- export type HtmlVarResult = { value: string | VNode<any>; ref: RefProps; node: VNode<any> };
4
-
3
+ export type HtmlVarValueProps = string | VNode<any> | (() => Promise<VNode<any>>);
4
+ export type HtmlVarResult = { value: HtmlVarValueProps; ref: RefProps; node: VNode<any> };
5
5
  export class HtmlVar implements HtmlVarResult {
6
- private _value: string | VNode<any>;
6
+ private _value: HtmlVarValueProps;
7
7
  private _dirty = false;
8
8
  private _ref: RefProps;
9
9
  private resolve!: () => void;
10
10
  private promise: Promise<void>;
11
11
 
12
- constructor(initial?: string | VNode<any>) {
12
+ constructor(initial?: HtmlVarValueProps) {
13
13
  this.promise = new Promise<void>((res) => {
14
14
  this.resolve = res;
15
15
  });
@@ -28,7 +28,8 @@ export class HtmlVar implements HtmlVarResult {
28
28
  }
29
29
 
30
30
  private async update(): Promise<void> {
31
- await this._ref.mountInnerComponent!(this._value);
31
+ const v = typeof this._value === 'function' ? await this._value() : this._value;
32
+ await this._ref.mountInnerComponent!(v);
32
33
  this._dirty = false;
33
34
  this._value = '';
34
35
  }
@@ -38,7 +39,7 @@ export class HtmlVar implements HtmlVarResult {
38
39
  await this.promise;
39
40
  }
40
41
 
41
- set value(value: string | VNode<any>) {
42
+ set value(value: HtmlVarValueProps) {
42
43
  this._value = value;
43
44
  if (this._dirty) {
44
45
  return;
@@ -55,7 +56,7 @@ export class HtmlVar implements HtmlVarResult {
55
56
  });
56
57
  }
57
58
 
58
- get value(): string | VNode<any> {
59
+ get value(): HtmlVarValueProps {
59
60
  return this._ref.current ? this._ref.current.innerHTML : this._value;
60
61
  }
61
62
 
@@ -64,13 +65,15 @@ export class HtmlVar implements HtmlVarResult {
64
65
  }
65
66
 
66
67
  get node(): VNode<any> {
67
- this._dirty = false;
68
+ // if value is a function, it will be loaded later in onLoad
69
+ const delayLoad = typeof this._value === 'function';
70
+ this._dirty = delayLoad ? true : false;
68
71
  // the Fragment Tag will be present in the html if ref is assigned
69
72
  return {
70
73
  type: 'Fragment',
71
74
  props: {
72
75
  ref: this._ref,
73
- children: this._value,
76
+ children: delayLoad ? '' : this._value,
74
77
  },
75
78
  html: [],
76
79
  };
@@ -6,6 +6,7 @@ export * from './drag-refresh';
6
6
  export * from './editable-label';
7
7
  export * from './float-window';
8
8
  export * from './grid';
9
+ export * from './html-load';
9
10
  export * from './html-var';
10
11
  export * from './input-with-title';
11
12
  export * from './link-item';
@@ -23,10 +24,14 @@ export * from './paging-link';
23
24
  export * from './panel';
24
25
  export * from './popup-menu';
25
26
  export * from './progress';
27
+ export * from './radio-label-component';
26
28
  export * from './redirect';
27
29
  export * from './resizable-splitter';
30
+ export * from './select-angle-component';
28
31
  export * from './select-with-title';
32
+ export * from './slide-tab-component';
29
33
  export * from './spinner';
34
+ export * from './stars-component';
30
35
  export * from './svg';
31
36
  export * from './tabs';
32
37
  export * from './text-glow';
@@ -0,0 +1,36 @@
1
+ import { bindGlobalStyle, CssProps } from 'lupine.components';
2
+
3
+ export const RadioLabelComponent = (props: {
4
+ label: string;
5
+ name: string;
6
+ checked?: boolean;
7
+ disabled?: boolean;
8
+ onChange?: (checked: boolean) => void;
9
+ className?: string;
10
+ radioClassname?: string;
11
+ }) => {
12
+ const css: CssProps = {
13
+ display: 'flex',
14
+ '& > label': {
15
+ display: 'flex',
16
+ alignItems: 'center',
17
+ },
18
+ };
19
+ bindGlobalStyle('radio-label-component', css);
20
+
21
+ return (
22
+ <div class={'radio-label-component' + (props.className ? ' ' + props.className : '')}>
23
+ <label>
24
+ <input
25
+ type='radio'
26
+ name={props.name}
27
+ class={'input-base input-s' + (props.radioClassname ? ' ' + props.radioClassname : '')}
28
+ checked={props.checked}
29
+ disabled={props.disabled}
30
+ onChange={(event) => props.onChange?.((event.target as HTMLInputElement).checked)}
31
+ />
32
+ <span class='ml-ss'>{props.label}</span>
33
+ </label>
34
+ </div>
35
+ );
36
+ };
@@ -0,0 +1,127 @@
1
+ import { CssProps, RefProps } from 'lupine.components';
2
+
3
+ export type SelectAngleComponentHookProps = {
4
+ setAngle?: (angle: number) => void;
5
+ };
6
+ export type SelectAngleComponentProps = {
7
+ size?: string;
8
+ angle: number;
9
+ onChange: (angle: number) => void;
10
+ hook?: SelectAngleComponentHookProps;
11
+ };
12
+ export const SelectAngleComponent = (props: SelectAngleComponentProps) => {
13
+ const css: CssProps = {
14
+ width: props.size || '80px',
15
+ height: props.size || '80px',
16
+ '&circle': {
17
+ width: '100%',
18
+ height: '100%',
19
+ borderRadius: '50%',
20
+ border: '2px solid #aaa',
21
+ position: 'relative',
22
+ backgroundColor: 'var(--primary-bg-color)',
23
+ cursor: 'pointer',
24
+ },
25
+ '&needle': {
26
+ width: '2px',
27
+ height: '50%',
28
+ backgroundColor: 'red',
29
+ position: 'absolute',
30
+ top: '0',
31
+ left: '50%',
32
+ transformOrigin: 'bottom center',
33
+ transform: 'rotate(90deg)',
34
+ },
35
+ '&tips': {
36
+ position: 'absolute',
37
+ top: '50%',
38
+ left: '50%',
39
+ transform: 'translate(-50%, -50%)',
40
+ fontSize: '12px',
41
+ color: 'var(--primary-color)',
42
+ fontWeight: '600',
43
+ zIndex: '10',
44
+ },
45
+ '&a0, &a90, &a180, &a270': {
46
+ width: '6px',
47
+ height: '6px',
48
+ borderRadius: '50%',
49
+ backgroundColor: '#333',
50
+ position: 'absolute',
51
+ top: '0',
52
+ left: '50%',
53
+ transform: 'translate(-50%, -50%)',
54
+ fontSize: '12px',
55
+ color: '#333',
56
+ },
57
+ '&a90': {
58
+ top: '50%',
59
+ left: '100%',
60
+ },
61
+ '&a180': {
62
+ top: '100%',
63
+ left: '50%',
64
+ },
65
+ '&a270': {
66
+ top: '50%',
67
+ left: '0',
68
+ },
69
+ };
70
+
71
+ let cx: number = 0;
72
+ let cy: number = 0;
73
+ let mv = false;
74
+ if (props.hook) {
75
+ props.hook.setAngle = (angle) => {
76
+ updateAngleSub(angle);
77
+ };
78
+ }
79
+ const updateAngle = (ev: MouseEvent) => {
80
+ const dx = ev.clientX - cx;
81
+ const dy = ev.clientY - cy;
82
+ // atan2 返回弧度,顺时针0°为右侧
83
+ let deg = Math.atan2(dy, dx) * (180 / Math.PI);
84
+ deg = (deg + 450) % 360; // 让上方为0°
85
+ updateAngleSub(deg);
86
+ };
87
+ const updateAngleSub = (deg: number) => {
88
+ const needle = ref.$('&needle');
89
+ const text = ref.$('&tips');
90
+ needle.style.transform = `rotate(${deg}deg)`;
91
+ text.textContent = `${deg.toFixed(0)}°`;
92
+ props.onChange(deg);
93
+ };
94
+
95
+ const pointerdown = (ev: MouseEvent) => {
96
+ const picker = ref.$('&circle');
97
+ const rect = picker.getBoundingClientRect();
98
+ cx = rect.left + rect.width / 2;
99
+ cy = rect.top + rect.height / 2;
100
+
101
+ updateAngle(ev);
102
+ mv = true;
103
+ };
104
+ const pointermove = (ev: MouseEvent) => {
105
+ if (!mv) {
106
+ return;
107
+ }
108
+ updateAngle(ev);
109
+ };
110
+ const pointerup = () => {
111
+ mv = false;
112
+ };
113
+ const ref: RefProps = {};
114
+ return (
115
+ <div ref={ref} css={css}>
116
+ <div class='&circle' onPointerDown={pointerdown} onPointerMove={pointermove} onPointerUp={pointerup}>
117
+ <div class='&needle'></div>
118
+ <div class='&tips'>90°</div>
119
+
120
+ <div class='&a0' onClick={() => updateAngleSub(0)}></div>
121
+ <div class='&a90' onClick={() => updateAngleSub(90)}></div>
122
+ <div class='&a180' onClick={() => updateAngleSub(180)}></div>
123
+ <div class='&a270' onClick={() => updateAngleSub(270)}></div>
124
+ </div>
125
+ </div>
126
+ );
127
+ };
@@ -0,0 +1,149 @@
1
+ import { CssProps, RefProps, VNode, bindGlobalStyle } from 'lupine.components';
2
+
3
+ export interface SlideTabProps {
4
+ pages: { title: string; content: VNode<any> }[];
5
+ };
6
+ export const SlideTabComponent = (props: SlideTabProps) => {
7
+ const css: CssProps = {
8
+ display: 'flex',
9
+ flexDirection: 'column',
10
+ flex: 1,
11
+ fontSize: '12px',
12
+ borderRadius: '6px',
13
+ padding: '0px 8px 4px 8px',
14
+ // marginBottom: '8px',
15
+ height: '100%',
16
+ '.slide-tab-c-list': {
17
+ flex: 1,
18
+ borderRadius: '6px',
19
+ display: 'flex',
20
+ overflowX: 'auto',
21
+ width: '100%',
22
+ scrollSnapType: 'x mandatory',
23
+ gap: '8px',
24
+ paddingBottom: '10px',
25
+ scrollBehavior: 'smooth',
26
+ WebkitOverflowScrolling: 'touch',
27
+ },
28
+ '.slide-tab-c-slide': {
29
+ width: '100%',
30
+ overflow: 'hidden',
31
+ position: 'relative',
32
+ minWidth: '100%',
33
+ flexShrink: 0,
34
+ scrollSnapAlign: 'start',
35
+ height: '100%',
36
+ overflowY: 'auto',
37
+ },
38
+
39
+ '.slide-tab-c-nav': {
40
+ display: 'flex',
41
+ flexDirection: 'row',
42
+ justifyContent: 'center',
43
+ backgroundColor: 'var(--primary-bg-color)',
44
+ position: 'sticky',
45
+ top: 0,
46
+ zIndex: 1,
47
+ },
48
+ '.slide-tab-c-nav-wrap': {
49
+ display: 'flex',
50
+ flexDirection: 'row',
51
+ justifyContent: 'center',
52
+ padding: '2px 4px',
53
+ borderRadius: '4px',
54
+ backgroundColor: 'var(--secondary-bg-color)',
55
+ },
56
+ '.slide-tab-c-nav-item': {
57
+ cursor: 'pointer',
58
+ padding: '4px 8px',
59
+ borderRadius: '4px',
60
+ marginRight: '8px',
61
+ },
62
+ '.slide-tab-c-nav-item.active': {
63
+ backgroundColor: 'var(--primary-accent-color)',
64
+ color: 'white',
65
+ },
66
+ };
67
+ bindGlobalStyle('slide-tab-c-box', css);
68
+
69
+ const ref: RefProps = {};
70
+ let slideIndex = 0;
71
+ let manualScroll = false;
72
+ let scrollEndTimer: NodeJS.Timeout | null = null;
73
+ const drawerScroll = () => {
74
+ if (manualScroll) {
75
+ return;
76
+ }
77
+ if (scrollEndTimer) {
78
+ clearTimeout(scrollEndTimer);
79
+ }
80
+ scrollEndTimer = setTimeout(() => {
81
+ drawerScrollStop();
82
+ }, 100);
83
+ };
84
+ const resetSlides = (index: number) => {
85
+ const dots = ref.$all('.slide-tab-c-nav-item');
86
+ for (let i = 0; i < dots.length; i++) {
87
+ dots[i].classList.toggle('active', i === index);
88
+ }
89
+ };
90
+ const drawerScrollStop = () => {
91
+ const drawer = ref.$('.slide-tab-c-list');
92
+ const width = drawer.clientWidth;
93
+ const currentScrollIndex = Math.round(drawer.scrollLeft / width);
94
+ slideIndex = currentScrollIndex;
95
+ resetSlides(slideIndex);
96
+ };
97
+ const moveSlide = (slideIndex: number) => {
98
+ const drawer = ref.$('.slide-tab-c-list');
99
+ const children = ref.$all('.slide-tab-c-slide');
100
+ if (!drawer || !children || children.length === 0) {
101
+ return;
102
+ }
103
+ const target = children[slideIndex];
104
+ if (!target) {
105
+ return;
106
+ }
107
+ const offsetLeft = target.offsetLeft;
108
+ manualScroll = true;
109
+ drawer.scrollTo({
110
+ left: offsetLeft,
111
+ behavior: 'smooth',
112
+ });
113
+
114
+ resetSlides(slideIndex);
115
+ setTimeout(() => {
116
+ manualScroll = false;
117
+ }, 300);
118
+ };
119
+
120
+ return (
121
+ <section class='slide-tab-c-box' ref={ref}>
122
+ <div class='slide-tab-c-nav'>
123
+ <div class='slide-tab-c-nav-wrap'>
124
+ {props.pages.map((page, index) => (
125
+ <div
126
+ class={`slide-tab-c-nav-item ${index === 0 ? 'active' : ''}`}
127
+ onClick={(event) => {
128
+ event.preventDefault();
129
+ moveSlide(index);
130
+ }}
131
+ >
132
+ {page.title}
133
+ </div>
134
+ ))}
135
+
136
+ </div>
137
+ </div>
138
+ <div class='slide-tab-c-list no-scrollbar-container' onScroll={drawerScroll}>
139
+
140
+ {props.pages.map((page) => (
141
+ <div class='slide-tab-c-slide no-scrollbar-container'>
142
+ {page.content}
143
+ </div>
144
+ ))}
145
+
146
+ </div>
147
+ </section>
148
+ );
149
+ };
@@ -0,0 +1,66 @@
1
+ import { bindGlobalStyle, CssProps, RefProps } from 'lupine.components';
2
+
3
+ export type StarsHookComponentProps = {
4
+ setValue: (value: number) => void;
5
+ getValue: () => number;
6
+ };
7
+ export type StarsComponentProps = {
8
+ maxLength: number;
9
+ value: number;
10
+ onChange?: (value: number) => void;
11
+ hook?: StarsHookComponentProps;
12
+ fontSize?: string;
13
+ };
14
+ export const StarsComponent = (props: StarsComponentProps) => {
15
+ const css: CssProps = {
16
+ display: 'flex',
17
+ flexDirection: 'row',
18
+ '.stars-label': {
19
+ color: '#9d9d9d',
20
+ cursor: 'pointer',
21
+ display: 'flex',
22
+ alignItems: 'center',
23
+ },
24
+ '.stars-label.active': {
25
+ color: 'blue',
26
+ },
27
+ '.stars-label .full, .stars-label.active .outline': {
28
+ display: 'none',
29
+ },
30
+ '.stars-label.active .full, .stars-label .outline': {
31
+ display: 'inline',
32
+ },
33
+ };
34
+ bindGlobalStyle('stars-box', css);
35
+
36
+ const setValue = (value: number) => {
37
+ props.value = value;
38
+ const stars = ref.$all('.stars-label') as NodeListOf<Element>;
39
+ stars.forEach((star, index) => {
40
+ star.classList.toggle('active', index < value);
41
+ });
42
+ };
43
+ if (props.hook) {
44
+ props.hook.setValue = (value) => {
45
+ setValue(value);
46
+ };
47
+ props.hook.getValue = () => props.value;
48
+ }
49
+ const ref: RefProps = {};
50
+ return (
51
+ <div style={{ fontSize: props.fontSize || '20px' }} ref={ref} class='stars-box'>
52
+ {Array.from({ length: props.maxLength }).map((value, index) => (
53
+ <label
54
+ class={'stars-label' + (index < props.value ? ' active' : '')}
55
+ onClick={() => {
56
+ setValue(index + 1);
57
+ props.onChange?.(index + 1);
58
+ }}
59
+ >
60
+ <i class='ifc-icon ma-cards-heart full'></i>
61
+ <i class='ifc-icon ma-cards-heart-outline outline'></i>
62
+ </label>
63
+ ))}
64
+ </div>
65
+ );
66
+ };