lupine.components 1.1.29 → 1.1.31

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.29",
3
+ "version": "1.1.31",
4
4
  "license": "MIT",
5
5
  "author": "uuware.com",
6
6
  "homepage": "https://github.com/uuware/lupine.js",
@@ -2,7 +2,7 @@ import { CssProps, RefProps } from 'lupine.web';
2
2
  import { DemoStory } from '../../demo/demo-types';
3
3
  import { IEditor } from './i-editor';
4
4
 
5
- const IEditorDemoPage = () => {
5
+ export const IEditorDemoPage = () => {
6
6
  let editor: IEditor | undefined;
7
7
 
8
8
  const ref: RefProps = {
@@ -3,7 +3,7 @@ import { DemoStory } from '../../demo/demo-types';
3
3
  import { PEditor } from './p-editor';
4
4
  import { PEditorOptions } from './p-editor-types';
5
5
 
6
- const PEditorDemoPage = (props: { args: PEditorOptions }) => {
6
+ export const PEditorDemoPage = (props: { args: PEditorOptions }) => {
7
7
  let editor: PEditor | undefined;
8
8
 
9
9
  const ref: RefProps = {
@@ -8,8 +8,8 @@ export type GaugeHighlightRange = {
8
8
  };
9
9
 
10
10
  export type GaugeHookProps = {
11
- setValue: (val: number | [number, number]) => void;
12
- getValue: () => number | [number, number];
11
+ setValue?: (val: number | [number, number]) => void;
12
+ getValue?: () => number | [number, number];
13
13
  };
14
14
 
15
15
  export type GaugeProps = {
@@ -1,12 +1,16 @@
1
- import { CssProps, RefProps } from 'lupine.web';
1
+ import { CssProps, RefProps, VNode } from 'lupine.web';
2
+ import { HtmlVar } from './html-var';
2
3
  import { ActionSheet, ActionSheetCloseProps, ActionSheetCloseReasonProps } from './action-sheet';
3
4
 
4
5
  export type ActionSheetDatePickerOrder = 'YMD' | 'DMY';
6
+ export type ActionSheetDatePickerFormat = 'YMD' | 'MD' | 'D';
5
7
 
6
8
  export type ActionSheetDatePickerProps = {
7
- /** Initial date value. Format: "YYYY-MM-DD" */
9
+ /** Initial date value. Format depends on format prop, defaults to "YYYY-MM-DD" */
8
10
  value?: string;
9
11
  title?: string;
12
+ /** Format of the picker. Default: 'YMD' */
13
+ format?: ActionSheetDatePickerFormat;
10
14
  /** Column order. Default: 'YMD' (Year | Month | Day) */
11
15
  order?: ActionSheetDatePickerOrder;
12
16
  /** Month labels. Defaults to English month names. Length determines month count. */
@@ -35,6 +39,7 @@ const PAD = Math.floor(VISIBLE / 2); // 2 rows of padding above/below
35
39
  export const ActionSheetDatePicker = async ({
36
40
  value = '',
37
41
  title = '',
42
+ format = 'YMD',
38
43
  order = 'YMD',
39
44
  months = DEFAULT_MONTHS,
40
45
  yearRange = 100,
@@ -44,7 +49,21 @@ export const ActionSheetDatePicker = async ({
44
49
  }: ActionSheetDatePickerProps): Promise<string | undefined> => {
45
50
  // Parse initial date
46
51
  const today = new Date();
47
- const parts = value.match(/^(\d{4})-(\d{2})-(\d{2})$/) ?? [];
52
+
53
+ let parts: string[] = [];
54
+ if (format === 'MD') {
55
+ parts = value.match(/^(\d{2})-(\d{2})$/) ?? [];
56
+ parts.unshift(''); // Shift empty year so indices [1]/[2] match month/day fallback logic below
57
+ parts.unshift(''); // Padding to push match groups to [2][3]
58
+ } else if (format === 'D') {
59
+ parts = value.match(/^(\d{2})$/) ?? [];
60
+ parts.unshift('');
61
+ parts.unshift('');
62
+ parts.unshift(''); // Push match group to [3]
63
+ } else {
64
+ parts = value.match(/^(\d{4})-(\d{2})-(\d{2})$/) ?? [];
65
+ }
66
+
48
67
  const initialYear = parts[1] ? parseInt(parts[1], 10) : today.getFullYear();
49
68
  const initialMonth = parts[2] ? parseInt(parts[2], 10) : today.getMonth() + 1;
50
69
  const initialDay = parts[3] ? parseInt(parts[3], 10) : today.getDate();
@@ -60,15 +79,27 @@ export const ActionSheetDatePicker = async ({
60
79
  const monthRef: RefProps = {};
61
80
  const dayRef: RefProps = {};
62
81
 
63
- const getIdx = (colRef: RefProps) => Math.round((colRef.current as HTMLElement).scrollTop / ITEM_H);
82
+ const getIdx = (colRef: RefProps) => {
83
+ if (!colRef.current) return 0;
84
+ return Math.round((colRef.current as HTMLElement).scrollTop / ITEM_H);
85
+ };
64
86
 
65
87
  const onConfirm = () => {
66
88
  const year = START_YEAR + getIdx(yearRef);
67
89
  const month = 1 + getIdx(monthRef);
68
90
  const day = 1 + getIdx(dayRef);
69
- const result = [String(year).padStart(4, '0'), String(month).padStart(2, '0'), String(day).padStart(2, '0')].join(
70
- '-'
71
- );
91
+
92
+ let result = '';
93
+ if (format === 'D') {
94
+ result = String(day).padStart(2, '0');
95
+ } else if (format === 'MD') {
96
+ result = [String(month).padStart(2, '0'), String(day).padStart(2, '0')].join('-');
97
+ } else {
98
+ result = [String(year).padStart(4, '0'), String(month).padStart(2, '0'), String(day).padStart(2, '0')].join(
99
+ '-'
100
+ );
101
+ }
102
+
72
103
  resolve(result);
73
104
  handleClose('confirm');
74
105
  };
@@ -80,7 +111,7 @@ export const ActionSheetDatePicker = async ({
80
111
  * Build a scrollable column.
81
112
  * Carousel pattern: scroll to item's offsetTop on load; read scrollTop on confirm.
82
113
  */
83
- const buildColumn = (labels: string[], initialIdx: number, colRef: RefProps) => {
114
+ const buildColumn = (labels: string[], initialIdx: number, colRef: RefProps, onScroll?: () => void) => {
84
115
  colRef.onLoad = async () => {
85
116
  const el = colRef.current as HTMLElement;
86
117
  const items = el.querySelectorAll('.dp-item');
@@ -91,7 +122,7 @@ export const ActionSheetDatePicker = async ({
91
122
  };
92
123
 
93
124
  return (
94
- <div class='col' ref={colRef}>
125
+ <div class='col' ref={colRef} onScroll={onScroll}>
95
126
  {Array.from({ length: PAD }, (_, i) => (
96
127
  <div class='dp-pad' key={`t${i}`} />
97
128
  ))}
@@ -107,18 +138,104 @@ export const ActionSheetDatePicker = async ({
107
138
  );
108
139
  };
109
140
 
141
+ const buildDynamicColumn = (labelsVar: HtmlVar, initialIdx: number, colRef: RefProps) => {
142
+ colRef.onLoad = async () => {
143
+ const el = colRef.current as HTMLElement;
144
+ const items = el.querySelectorAll('.dp-item');
145
+ if (items[initialIdx]) {
146
+ el.scrollTop = (items[initialIdx] as HTMLElement).offsetTop - PAD * ITEM_H;
147
+ }
148
+ };
149
+
150
+ return (
151
+ <div class='col' ref={colRef}>
152
+ {labelsVar.node}
153
+ </div>
154
+ );
155
+ };
156
+
157
+ const generateDaysDOM = (maxDays: number) => {
158
+ const labels = Array.from({ length: maxDays }, (_, i) => String(i + 1).padStart(2, '0'));
159
+ return (
160
+ <>
161
+ {Array.from({ length: PAD }, (_, i) => (
162
+ <div class='dp-pad' key={`t${i}`} />
163
+ ))}
164
+ {labels.map((label: string, i: number) => (
165
+ <div class='dp-item' key={i}>
166
+ {label}
167
+ </div>
168
+ ))}
169
+ {Array.from({ length: PAD }, (_, i) => (
170
+ <div class='dp-pad' key={`b${i}`} />
171
+ ))}
172
+ </>
173
+ );
174
+ };
175
+
176
+ const getMaxDays = (year: number, month: number) => {
177
+ if (format === 'D') return 31;
178
+ if (format === 'MD') return new Date(2000, month, 0).getDate(); // Leap year forced max
179
+ return new Date(year, month, 0).getDate();
180
+ };
181
+
182
+ let currentMaxDays = 0;
183
+ let scrollTimeout: any;
184
+ const handleDateScroll = () => {
185
+ clearTimeout(scrollTimeout);
186
+ scrollTimeout = setTimeout(() => {
187
+ // Read current selected year and month based on scroll heights
188
+ const curYear = format === 'MD' ? 2000 : START_YEAR + getIdx(yearRef);
189
+ const curMonth = 1 + getIdx(monthRef);
190
+
191
+ // Calculate the maximum allowed days for this selection
192
+ const maxDays = getMaxDays(curYear, curMonth);
193
+
194
+ // If the number of days changed, update the reactive var
195
+ if (currentMaxDays !== maxDays) {
196
+ currentMaxDays = maxDays;
197
+ daysVar.value = generateDaysDOM(maxDays);
198
+
199
+ // If the currently selected day is now out of bounds (e.g. Feb 30th), clamp it down.
200
+ const currentDayIdx = getIdx(dayRef);
201
+ if (currentDayIdx >= maxDays) {
202
+ setTimeout(() => {
203
+ if (dayRef.current) {
204
+ const el = dayRef.current as HTMLElement;
205
+ const items = el.querySelectorAll('.dp-item');
206
+ const targetItem = items[maxDays - 1] as HTMLElement;
207
+ if (targetItem) {
208
+ el.scrollTo({ top: targetItem.offsetTop - PAD * ITEM_H, behavior: 'smooth' });
209
+ }
210
+ }
211
+ }, 50);
212
+ }
213
+ }
214
+ }, 150);
215
+ };
216
+
110
217
  const years = Array.from({ length: YEAR_COUNT }, (_, i) => String(START_YEAR + i));
111
- const days = Array.from({ length: 31 }, (_, i) => String(i + 1).padStart(2, '0'));
112
218
 
113
219
  const initialYearIdx = Math.max(0, Math.min(YEAR_COUNT - 1, initialYear - START_YEAR));
114
220
  const initialMonthIdx = Math.max(0, Math.min(months.length - 1, initialMonth - 1));
115
- const initialDayIdx = Math.max(0, Math.min(30, initialDay - 1));
221
+ currentMaxDays = getMaxDays(START_YEAR + initialYearIdx, initialMonthIdx + 1);
116
222
 
117
- const yearCol = buildColumn(years, initialYearIdx, yearRef);
118
- const monthCol = buildColumn(months, initialMonthIdx, monthRef);
119
- const dayCol = buildColumn(days, initialDayIdx, dayRef);
223
+ // Day array is reactive, max based on initial selection constraints
224
+ const daysVar = new HtmlVar(generateDaysDOM(currentMaxDays));
225
+ const initialDayIdx = Math.max(0, Math.min(currentMaxDays - 1, initialDay - 1));
120
226
 
121
- const orderedCols = order === 'DMY' ? [dayCol, monthCol, yearCol] : [yearCol, monthCol, dayCol];
227
+ const yearCol = buildColumn(years, initialYearIdx, yearRef, handleDateScroll);
228
+ const monthCol = buildColumn(months, initialMonthIdx, monthRef, handleDateScroll);
229
+ const dayCol = buildDynamicColumn(daysVar, initialDayIdx, dayRef);
230
+
231
+ let orderedCols: any[] = [];
232
+ if (format === 'D') {
233
+ orderedCols = [dayCol];
234
+ } else if (format === 'MD') {
235
+ orderedCols = order === 'DMY' ? [dayCol, monthCol] : [monthCol, dayCol];
236
+ } else {
237
+ orderedCols = order === 'DMY' ? [dayCol, monthCol, yearCol] : [yearCol, monthCol, dayCol];
238
+ }
122
239
 
123
240
  const pickerBodyCss: CssProps = {
124
241
  borderTop: '1px solid var(--primary-border-color)',
@@ -162,11 +279,7 @@ export const ActionSheetDatePicker = async ({
162
279
 
163
280
  const pickerBody = (
164
281
  <div css={pickerBodyCss}>
165
- <div class='picker-body'>
166
- {orderedCols[0]}
167
- {orderedCols[1]}
168
- {orderedCols[2]}
169
- </div>
282
+ <div class='picker-body'>{orderedCols}</div>
170
283
  </div>
171
284
  );
172
285
 
@@ -5,6 +5,7 @@ import {
5
5
  ActionSheetMessage,
6
6
  ActionSheetInput,
7
7
  ActionSheetSelectOptionsProps,
8
+ ActionSheetSelectWrapPromise,
8
9
  } from './action-sheet';
9
10
  import { ActionSheetTimePicker } from './action-sheet-time';
10
11
  import { ActionSheetDatePicker } from './action-sheet-date';
@@ -84,6 +85,18 @@ export const actionSheetDemo: DemoStory<any> = {
84
85
  });
85
86
  }}
86
87
  />
88
+ <Button
89
+ text='Show Action Sheet Wrap Select'
90
+ size={ButtonSize.Medium}
91
+ onClick={async () => {
92
+ const days = Array.from({ length: 31 }, (_, i) => String(i + 1));
93
+ const result = await ActionSheetSelectWrapPromise({
94
+ title: 'Select a Day',
95
+ options: days,
96
+ });
97
+ if (result >= 0) console.log('Wrap Select picked:', days[result]);
98
+ }}
99
+ />
87
100
  <Button
88
101
  text='Show Time Picker (HH:mm)'
89
102
  size={ButtonSize.Medium}
@@ -132,6 +145,30 @@ export const actionSheetDemo: DemoStory<any> = {
132
145
  if (result) console.log('Date selected (DMY):', result);
133
146
  }}
134
147
  />
148
+ <Button
149
+ text='Show Date Picker (Month-Day)'
150
+ size={ButtonSize.Medium}
151
+ onClick={async () => {
152
+ const result = await ActionSheetDatePicker({
153
+ title: 'Pick Month and Day',
154
+ value: '02-24',
155
+ format: 'MD',
156
+ });
157
+ if (result) console.log('Date selected (MD):', result);
158
+ }}
159
+ />
160
+ <Button
161
+ text='Show Date Picker (Day Only)'
162
+ size={ButtonSize.Medium}
163
+ onClick={async () => {
164
+ const result = await ActionSheetDatePicker({
165
+ title: 'Pick a Day',
166
+ value: '24',
167
+ format: 'D',
168
+ });
169
+ if (result) console.log('Date selected (D):', result);
170
+ }}
171
+ />
135
172
  <Button
136
173
  text='Show Color Picker'
137
174
  size={ButtonSize.Medium}
@@ -141,6 +141,29 @@ export class ActionSheet {
141
141
  marginTop: 'unset',
142
142
  maxWidth: 'unset',
143
143
  },
144
+ '.act-sheet-wrap-container': {
145
+ display: 'flex',
146
+ flexDirection: 'row',
147
+ flexWrap: 'wrap',
148
+ alignItems: 'center',
149
+ justifyContent: 'center',
150
+ padding: '16px',
151
+ },
152
+ '.act-sheet-wrap-item': {
153
+ cursor: 'pointer',
154
+ padding: '8px',
155
+ borderRadius: '8px',
156
+ border: '1px solid var(--primary-border-color)',
157
+ margin: '4px',
158
+ width: 'calc(25% - 8px)',
159
+ minWidth: '60px',
160
+ textAlign: 'center',
161
+ transition: 'all 0.3s ease',
162
+ },
163
+ '.act-sheet-wrap-item:hover': {
164
+ fontWeight: 'bold',
165
+ backgroundColor: 'var(--primary-bg-color-hover, rgba(0,0,0,0.04))',
166
+ },
144
167
  },
145
168
  };
146
169
  const component = (
@@ -448,6 +471,46 @@ export const ActionSheetSelectPromise = async ({
448
471
  });
449
472
  };
450
473
 
474
+ export const ActionSheetSelectWrapPromise = async ({
475
+ title,
476
+ contentMaxWidth,
477
+ contentMaxHeight,
478
+ options = ActionSheetSelectOptionsProps.Ok,
479
+ closeWhenClickOutside = true,
480
+ cancelButtonText = 'Cancel',
481
+ zIndex,
482
+ }: ActionSheetSelectPromiseProps): Promise<number> => {
483
+ return new Promise(async (resolve, reject) => {
484
+ const handleClicked = async (index: number, close: ActionSheetCloseProps) => {
485
+ resolve(index);
486
+ close('select');
487
+ };
488
+ const closeEvent = (reason?: ActionSheetCloseReasonProps) => {
489
+ if (reason !== 'select') {
490
+ resolve(-1);
491
+ }
492
+ };
493
+ const handleClose = await ActionSheet.show({
494
+ title,
495
+ children: (
496
+ <div class='act-sheet-wrap-container'>
497
+ {options.map((option, index) => (
498
+ <div class='act-sheet-wrap-item' key={index} onClick={() => handleClicked(index, handleClose)}>
499
+ {option}
500
+ </div>
501
+ ))}
502
+ </div>
503
+ ),
504
+ contentMaxWidth,
505
+ contentMaxHeight,
506
+ cancelButtonText,
507
+ closeEvent,
508
+ closeWhenClickOutside,
509
+ zIndex,
510
+ });
511
+ });
512
+ };
513
+
451
514
  export type ActionSheetMultiSelectPromiseProps = {
452
515
  title: string | VNode<any>;
453
516
  contentMaxWidth?: string;
@@ -8,6 +8,7 @@ export class HtmlVar implements HtmlVarResult {
8
8
  private _ref: RefProps;
9
9
  private resolve!: () => void;
10
10
  private promise: Promise<void>;
11
+ private updatePromise: Promise<void> | null = null;
11
12
 
12
13
  constructor(initial?: HtmlVarValueProps) {
13
14
  this.promise = new Promise<void>((res) => {
@@ -28,10 +29,23 @@ export class HtmlVar implements HtmlVarResult {
28
29
  }
29
30
 
30
31
  private async update(): Promise<void> {
31
- const v = typeof this._value === 'function' ? await this._value() : this._value;
32
- await this._ref.mountInnerComponent!(v);
33
- this._dirty = false;
34
- this._value = '';
32
+ if (this.updatePromise) {
33
+ return this.updatePromise;
34
+ }
35
+
36
+ this.updatePromise = (async () => {
37
+ while (this._dirty) {
38
+ this._dirty = false;
39
+ const v = typeof this._value === 'function' ? await this._value() : this._value;
40
+ await this._ref.mountInnerComponent!(v);
41
+ if (!this._dirty) {
42
+ this._value = '';
43
+ }
44
+ }
45
+ })();
46
+
47
+ await this.updatePromise;
48
+ this.updatePromise = null;
35
49
  }
36
50
 
37
51
  // need to wait before use ref.current
@@ -1,12 +1,18 @@
1
+ export * from './action-sheet-color';
2
+ export * from './action-sheet-date';
3
+ export * from './action-sheet-time';
1
4
  export * from './action-sheet';
2
5
  export * from './button-push-animation';
3
6
  export * from './button';
7
+ export * from './desktop-footer';
8
+ export * from './desktop-header';
4
9
  export * from './drag-refresh';
5
10
  export * from './editable-label';
6
11
  export * from './float-window';
7
12
  export * from './grid';
8
13
  export * from './html-load';
9
14
  export * from './html-var';
15
+ export * from './input-number';
10
16
  export * from './input-with-title';
11
17
  export * from './link-item';
12
18
  export * from './link-list';
@@ -82,7 +82,7 @@ export const MobileHeaderComponent = (props: any) => {
82
82
  width: '100%',
83
83
  height: 'auto',
84
84
  // padding: '2px 0',
85
- // boxShadow: 'var(--mobile-header-shadow)', // 第二项 2px 的话,顶部有阴影(线)
85
+ // boxShadow: 'var(--mobile-header-shadow)', // if 2px, then there is a shadow (line) on the top
86
86
  '& > *': {
87
87
  height: '100%',
88
88
  },
@@ -59,6 +59,7 @@ export const MobileHeaderTitleIcon = ({
59
59
  color: color || 'var(--primary-color)',
60
60
  background: background || 'var(--activatable-bg-color-normal)',
61
61
  boxShadow: noShadow ? 'unset' : 'var(--mobile-header-shadow)',
62
+ position: 'relative',
62
63
  zIndex: 'var(--layer-inside)', // bring boxShadow to front
63
64
  '.mhti-title': {
64
65
  display: 'flex',
@@ -1,9 +1,9 @@
1
- import { backActionHelper, CssProps, RefProps, VNode } from 'lupine.components';
1
+ import { backActionHelper, CssProps, RefProps, VNode, MediaQueryRange } from 'lupine.components';
2
2
 
3
3
  export class MobileSideMenuHelper {
4
4
  static show() {
5
5
  const ref = document.querySelector('.mobile-side-menu-mask') as HTMLDivElement;
6
- if (!ref) return;
6
+ if (!ref || ref.clientWidth > 100) return;
7
7
 
8
8
  ref.classList.add('show');
9
9
 
@@ -170,7 +170,7 @@ export class MobileSideMenuHelper {
170
170
  });
171
171
  }
172
172
  }
173
- export const MobileSideMenu = (props: { children: VNode<any> }) => {
173
+ export const MobileSideMenu = (props: { children: VNode<any>; autoExtend?: boolean }) => {
174
174
  const css: CssProps = {
175
175
  '.mobile-side-menu-mask': {
176
176
  display: 'none',
@@ -206,10 +206,26 @@ export const MobileSideMenu = (props: { children: VNode<any> }) => {
206
206
  overflowY: 'auto',
207
207
  transform: 'translateX(-100%)',
208
208
  boxShadow: 'var(--cover-box-shadow)',
209
- // trick: to put two padding-top properties
210
209
  'padding-top ': 'constant(safe-area-inset-top)',
211
210
  'padding-top': 'env(safe-area-inset-top)',
212
211
  },
212
+ [MediaQueryRange.TabletAbove]: {
213
+ '&.auto-extend .mobile-side-menu-mask': {
214
+ display: 'flex !important',
215
+ width: 'var(--auto-sidemenu-left-offset, 260px)',
216
+ right: 'auto',
217
+ backgroundColor: 'transparent !important',
218
+ pointerEvents: 'none',
219
+ },
220
+ '&.auto-extend .mobile-side-menu': {
221
+ transform: 'translateX(0) !important',
222
+ width: '100%',
223
+ maxWidth: 'unset',
224
+ boxShadow: 'none',
225
+ borderRight: '1px solid var(--primary-border-color)',
226
+ pointerEvents: 'auto',
227
+ },
228
+ },
213
229
  };
214
230
 
215
231
  const onClickContainer = (event: Event) => {
@@ -226,7 +242,7 @@ export const MobileSideMenu = (props: { children: VNode<any> }) => {
226
242
  },
227
243
  };
228
244
  return (
229
- <div css={css} ref={ref}>
245
+ <div css={css} ref={ref} class={['mobile-side-menu-top', props.autoExtend ? 'auto-extend' : ''].join(' ').trim()}>
230
246
  {/* <SliderFrame hook={props.sliderFrameHook} /> */}
231
247
  <div class='mobile-side-menu-mask' onClick={onClickContainer}>
232
248
  <div class='mobile-side-menu'>{props.children}</div>
@@ -19,7 +19,7 @@ export const MobileTopSysIcon = () => {
19
19
  },
20
20
  };
21
21
  return (
22
- <div css={css} onClick={() => MobileSideMenuHelper.show()}>
22
+ <div css={css} onClick={() => MobileSideMenuHelper.show()} class='mobile-top-sys-icon'>
23
23
  <i class='ifc-icon bs-list'></i>
24
24
  </div>
25
25
  );
@@ -23,6 +23,7 @@ export type ToggleBaseProps = {
23
23
  hook?: ToggleBaseHookProps;
24
24
  noToggle?: boolean; // if true, it will be like a button
25
25
  children: VNode<any>;
26
+ className?: string;
26
27
  };
27
28
  export const ToggleBase = (props: ToggleBaseProps) => {
28
29
  const applyToggle = (checked: boolean, disabled: boolean) => {
@@ -70,6 +71,7 @@ export const ToggleBase = (props: ToggleBaseProps) => {
70
71
  }
71
72
 
72
73
  const css: CssProps = {
74
+ display: 'inline-block',
73
75
  '.toggle-base-box, .toggle-base-container': {
74
76
  position: 'relative',
75
77
  width: `100%`,
@@ -91,7 +93,7 @@ export const ToggleBase = (props: ToggleBaseProps) => {
91
93
  width: `${typeof props.size.w === 'number' ? props.size.w + 'px' : props.size.w}`,
92
94
  height: `${typeof props.size.h === 'number' ? props.size.h + 'px' : props.size.h}`,
93
95
  }}
94
- class='toggle-base-component'
96
+ class={['toggle-base-component', props.className].join(' ').trim()}
95
97
  >
96
98
  <label class='toggle-base-box'>
97
99
  <div class='toggle-base-container'>{props.children}</div>
@@ -192,6 +194,7 @@ export type TogglePlayButtonProps = {
192
194
  textColor?: string;
193
195
  backgroundColor?: string;
194
196
  noWave?: boolean;
197
+ className?: string;
195
198
  };
196
199
  export const TogglePlayButton = (props: TogglePlayButtonProps) => {
197
200
  const css: CssProps = {
@@ -258,6 +261,7 @@ export type ToggleButtonProps = {
258
261
  checked?: boolean;
259
262
  onClick?: (checked: boolean) => void;
260
263
  hook?: ToggleBaseHookProps;
264
+ className?: string;
261
265
  };
262
266
  export const ToggleButton = (props: ToggleButtonProps) => {
263
267
  const css: CssProps = {
@@ -350,6 +354,7 @@ export type ToggleIconProps = {
350
354
  onClick?: (checked: boolean) => void;
351
355
  hook?: ToggleBaseHookProps;
352
356
  noToggle?: boolean; // if true, it will be like a button
357
+ className?: string;
353
358
  };
354
359
  export const ToggleIcon = (props: ToggleIconProps) => {
355
360
  const css: CssProps = {
@@ -421,7 +426,7 @@ export const ToggleIcon = (props: ToggleIconProps) => {
421
426
  };
422
427
  bindGlobalStyle('toggle-icon-component', css);
423
428
  const iconStyle = {
424
- fontSize: typeof props.size.h === 'number' ? `${Number((props.size.h * 0.55).toFixed(2))}px` : '1rem',
429
+ fontSize: typeof props.size.h === 'number' ? `${Number((props.size.h * 0.75).toFixed(2))}px` : '1rem',
425
430
  padding: props.size.padding || '4px 12px',
426
431
  };
427
432
 
@@ -55,7 +55,7 @@ const sideMenuMockCss: CssProps = {
55
55
  justifyContent: 'space-between',
56
56
  },
57
57
  '.msm-footer-cfg .msm-item': {
58
- padding: '12px 0px',
58
+ padding: '12px',
59
59
  },
60
60
  '.msm-footer-version': {
61
61
  textAlign: 'center',
@@ -1,3 +1,3 @@
1
1
  export * from './responsive-frame';
2
- export * from './top-frame';
3
2
  export * from './slider-frame';
3
+ export * from './top-frame';
@@ -15,10 +15,12 @@ export interface ResponsiveFrameProps {
15
15
  mainContent: VNode<any>;
16
16
  desktopHeaderTitle: string;
17
17
  desktopFooterTitle?: string;
18
- desktopTopMenu: IconMenuItemProps[];
19
- mobileBottomMenu: IconMenuItemProps[];
18
+ desktopTopMenu?: IconMenuItemProps[];
19
+ mobileBottomMenu?: IconMenuItemProps[];
20
20
  mobileSideMenuContent: VNode<any>;
21
21
  sharedContents: VNode<any>;
22
+ maxWidth?: string;
23
+ autoExtendSidemenu?: boolean;
22
24
  }
23
25
  export const ResponsiveFrame = (props: ResponsiveFrameProps) => {
24
26
  const cssContainer: CssProps = {
@@ -27,6 +29,11 @@ export const ResponsiveFrame = (props: ResponsiveFrameProps) => {
27
29
  width: '100%',
28
30
  height: '100%',
29
31
  minHeight: '100%',
32
+ maxWidth: props.maxWidth || '',
33
+ borderRight: props.maxWidth ? '1px solid var(--primary-border-color)' : 'none',
34
+ borderLeft: props.maxWidth ? '1px solid var(--primary-border-color)' : 'none',
35
+ margin: '0 auto',
36
+ overflowX: 'hidden',
30
37
  '.frame-top-menu': {
31
38
  display: 'flex',
32
39
  flexDirection: 'column',
@@ -56,26 +63,48 @@ export const ResponsiveFrame = (props: ResponsiveFrameProps) => {
56
63
  '.content-block .padding-block': {
57
64
  padding: '0 16px',
58
65
  },
66
+ [MediaQueryRange.DesktopAbove]: {
67
+ // for big screens, remove the shadow on the top of mobile header
68
+ '.mobile-header-title-icon-top': {
69
+ boxShadow: 'unset',
70
+ },
71
+ },
59
72
  [MediaQueryRange.TabletBelow]: {
60
73
  '.frame-footer .d-footer-box, .frame-top-menu .desktop-menu-box': {
61
74
  display: 'none',
62
75
  },
63
76
  },
77
+ [MediaQueryRange.TabletAbove]: {
78
+ transform: 'translateX(0)', // Traps position: fixed children inside this container's dimensions
79
+ '&.auto-extend': {
80
+ '--auto-sidemenu-left-offset': '260px',
81
+ paddingLeft: 'var(--auto-sidemenu-left-offset)',
82
+ boxSizing: 'border-box',
83
+ },
84
+ '.mobile-top-sys-icon': {
85
+ display: 'none',
86
+ },
87
+ },
64
88
  };
65
89
 
66
90
  return (
67
- <div css={cssContainer} class='responsive-frame'>
91
+ <div
92
+ css={cssContainer}
93
+ class={['responsive-frame', props.autoExtendSidemenu ? 'auto-extend' : ''].join(' ').trim()}
94
+ >
68
95
  {props.sharedContents}
69
96
  <div class='frame-top-menu'>
70
- <DesktopHeader title={props.desktopHeaderTitle} items={props.desktopTopMenu}></DesktopHeader>
97
+ {props.desktopTopMenu && (
98
+ <DesktopHeader title={props.desktopHeaderTitle} items={props.desktopTopMenu}></DesktopHeader>
99
+ )}
71
100
  <MobileHeaderComponent></MobileHeaderComponent>
72
101
  </div>
73
102
  <div class='frame-content'>
74
- <MobileSideMenu>{props.mobileSideMenuContent}</MobileSideMenu>
103
+ <MobileSideMenu autoExtend={props.autoExtendSidemenu}>{props.mobileSideMenuContent}</MobileSideMenu>
75
104
  <div class={'content-block ' + props.placeholderClassname}>{props.mainContent}</div>
76
105
  <div class='frame-footer'>
77
106
  {props.desktopFooterTitle && <DesktopFooter title={props.desktopFooterTitle}></DesktopFooter>}
78
- <MobileFooterMenu items={props.mobileBottomMenu}></MobileFooterMenu>
107
+ {props.mobileBottomMenu && <MobileFooterMenu items={props.mobileBottomMenu}></MobileFooterMenu>}
79
108
  </div>
80
109
  </div>
81
110
  </div>
@@ -14,6 +14,8 @@
14
14
  */
15
15
  import { VNode, CssProps, HtmlVar, RefProps, stopPropagation, MediaQueryRange } from 'lupine.components';
16
16
 
17
+ // addClass(SliderFramePosition) is used to show two SliderFrames for big screens,
18
+ // so when the second is showing, it needs to set this on the first one
17
19
  export type SliderFramePosition = 'desktop-slide-left' | 'desktop-slide-right';
18
20
  export type SliderFrameHookProps = {
19
21
  load?: (children: VNode<any>) => void;
@@ -62,7 +64,7 @@ export const SliderFrame = (props: SliderFrameProps) => {
62
64
  flexDirection: 'column',
63
65
  position: 'fixed',
64
66
  top: '0',
65
- left: '0',
67
+ left: 'var(--auto-sidemenu-left-offset, 0px)',
66
68
  right: '0',
67
69
  bottom: '0',
68
70
  zIndex: 'var(--layer-slider)',
@@ -75,8 +77,12 @@ export const SliderFrame = (props: SliderFrameProps) => {
75
77
  '&.show': {
76
78
  transform: props.direction === 'bottom' ? 'translateY(0)' : 'translateX(0)',
77
79
  },
80
+ '& > *': {
81
+ '--auto-sidemenu-left-offset': '0px',
82
+ },
78
83
  '& > fragment': {
79
84
  height: '100%',
85
+ '--auto-sidemenu-left-offset': '0px',
80
86
  },
81
87
  '&.desktop-slide-left': {
82
88
  [MediaQueryRange.TabletAbove]: {
@@ -87,8 +93,8 @@ export const SliderFrame = (props: SliderFrameProps) => {
87
93
  },
88
94
  '&.desktop-slide-right': {
89
95
  [MediaQueryRange.TabletAbove]: {
90
- top: '59px',
91
- left: '30%',
96
+ top: '59px', // Just below DesktopHeader
97
+ left: 'calc(max(var(--auto-sidemenu-left-offset, 0px), 30%))',
92
98
  transform: 'translateX(0)',
93
99
  // notice: here is connected with mobile-header-title-icon.tsx
94
100
  '.mobile-header-title-icon-top': {