create-lupine 1.0.13 → 1.0.15
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/index.js +32 -2
- package/package.json +1 -1
- package/templates/common/AI_CONTEXT.md +245 -50
- package/templates/common/dev/cp-folder.js +2 -6
- package/templates/cv-starter/web/src/index.html +1 -1
- package/templates/doc-starter/web/src/index.html +1 -1
- package/templates/hello-world/web/src/index.html +1 -1
- package/templates/hello-world/web/src/index.tsx +54 -15
- package/templates/hello-world/web/src/styles/global.css +63 -0
- package/templates/responsive-starter/api/package.json +6 -0
- package/templates/responsive-starter/api/resources/config_default.json +6 -0
- package/templates/responsive-starter/api/resources/install.sqlite.sql +4 -0
- package/templates/responsive-starter/api/src/index.ts +4 -0
- package/templates/responsive-starter/api/src/service/root-api.ts +18 -0
- package/templates/responsive-starter/lupine.json +23 -0
- package/templates/responsive-starter/web/assets/favicon.ico +0 -0
- package/templates/responsive-starter/web/package.json +6 -0
- package/templates/responsive-starter/web/src/app-icons.ts +72 -0
- package/templates/responsive-starter/web/src/components/input-history-component.tsx +93 -0
- package/templates/responsive-starter/web/src/components/mine-about-page.tsx +86 -0
- package/templates/responsive-starter/web/src/components/mine-premium-page.tsx +80 -0
- package/templates/responsive-starter/web/src/components/mine-settings-page.tsx +219 -0
- package/templates/responsive-starter/web/src/components/mobile-top-search-menu.tsx +119 -0
- package/templates/responsive-starter/web/src/components/note-detail.tsx +116 -0
- package/templates/responsive-starter/web/src/components/note-edit.tsx +154 -0
- package/templates/responsive-starter/web/src/components/note-search-page.tsx +193 -0
- package/templates/responsive-starter/web/src/components/search-input.tsx +95 -0
- package/templates/responsive-starter/web/src/components/side-menu-content.tsx +178 -0
- package/templates/responsive-starter/web/src/frames/app-responsive-frame.tsx +46 -0
- package/templates/responsive-starter/web/src/index.html +16 -0
- package/templates/responsive-starter/web/src/index.tsx +42 -0
- package/templates/responsive-starter/web/src/pages/about-page.tsx +43 -0
- package/templates/responsive-starter/web/src/pages/finance-page.tsx +46 -0
- package/templates/responsive-starter/web/src/pages/home-page.tsx +452 -0
- package/templates/responsive-starter/web/src/pages/mine-page.tsx +325 -0
- package/templates/responsive-starter/web/src/pages/tools-page.tsx +46 -0
- package/templates/responsive-starter/web/src/services/local-notes-service.ts +87 -0
- package/templates/responsive-starter/web/src/services/mine-service.ts +45 -0
- package/templates/responsive-starter/web/src/styles/app.css +0 -0
- package/templates/responsive-starter/web/src/styles/base-css.ts +65 -0
- package/templates/responsive-starter/web/src/styles/global.css +2143 -0
- package/templates/responsive-starter/web/src/styles/theme.ts +16 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { CssProps, HeaderWithBackFrame, HtmlVar, RefProps, SliderFrame, SliderFrameHookProps } from 'lupine.components';
|
|
2
|
+
import { SearchInput, SearchInputHookProps } from './search-input';
|
|
3
|
+
import { InputHistoryComponent, addHistoryItem } from './input-history-component';
|
|
4
|
+
import { LocalNoteProps, LocalNotesService } from '../services/local-notes-service';
|
|
5
|
+
import { NoteDetailComponent } from './note-detail';
|
|
6
|
+
import { clearHistoryList } from './input-history-component';
|
|
7
|
+
|
|
8
|
+
const extractText = (html: string) => {
|
|
9
|
+
const tmp = document.createElement('DIV');
|
|
10
|
+
tmp.innerHTML = html;
|
|
11
|
+
return tmp.textContent || tmp.innerText || '';
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const NoteSearchComponent = (props: { sliderFrameHook: SliderFrameHookProps }) => {
|
|
15
|
+
const listDom = new HtmlVar('');
|
|
16
|
+
const historyDom = new HtmlVar('');
|
|
17
|
+
|
|
18
|
+
const searchInputHook: SearchInputHookProps = {};
|
|
19
|
+
// this is the inner slide hook for editing from search results
|
|
20
|
+
const subSliderHook: SliderFrameHookProps = {};
|
|
21
|
+
|
|
22
|
+
const historyKey = 'note-search';
|
|
23
|
+
|
|
24
|
+
const onSearch = (value?: string) => {
|
|
25
|
+
const val = (value || '').trim();
|
|
26
|
+
if (val) {
|
|
27
|
+
addHistoryItem(historyKey, val);
|
|
28
|
+
hideHistory();
|
|
29
|
+
} else {
|
|
30
|
+
showHistory();
|
|
31
|
+
}
|
|
32
|
+
refreshList(val);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const onClearSearch = () => {
|
|
36
|
+
onSearch('');
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const showHistory = () => {
|
|
40
|
+
historyDom.value = (
|
|
41
|
+
<InputHistoryComponent
|
|
42
|
+
historyKey={historyKey}
|
|
43
|
+
onClearHistory={() => {
|
|
44
|
+
clearHistoryList(historyKey);
|
|
45
|
+
showHistory();
|
|
46
|
+
}}
|
|
47
|
+
onItemClick={(item) => {
|
|
48
|
+
searchInputHook.setValue!(item);
|
|
49
|
+
onSearch(item);
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const hideHistory = () => {
|
|
56
|
+
historyDom.value = '';
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const onViewNote = (note: LocalNoteProps) => {
|
|
60
|
+
subSliderHook.load!(
|
|
61
|
+
<NoteDetailComponent
|
|
62
|
+
note={note}
|
|
63
|
+
sliderFrameHook={subSliderHook}
|
|
64
|
+
onSaved={() => {
|
|
65
|
+
// We re-query the note values after saving. Easiest is to trigger search again.
|
|
66
|
+
const val = document.querySelector('.search-input .search-in-input') as HTMLInputElement;
|
|
67
|
+
if (val) onSearch(val.value);
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const refreshList = (query: string) => {
|
|
74
|
+
if (!query) {
|
|
75
|
+
listDom.value = '';
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const allNotes = LocalNotesService.getAllNotes();
|
|
80
|
+
const filteredNotes = allNotes.filter(
|
|
81
|
+
(n) =>
|
|
82
|
+
n.title.toLowerCase().includes(query.toLowerCase()) || n.content.toLowerCase().includes(query.toLowerCase())
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (filteredNotes.length === 0) {
|
|
86
|
+
listDom.value = (
|
|
87
|
+
<div
|
|
88
|
+
class='note-empty-state'
|
|
89
|
+
style={{ marginTop: '40px', textAlign: 'center', color: 'var(--secondary-color)' }}
|
|
90
|
+
>
|
|
91
|
+
No search results
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
listDom.value = (
|
|
98
|
+
<div class='note-list-container' style={{ paddingTop: '16px' }}>
|
|
99
|
+
{filteredNotes.map((note) => (
|
|
100
|
+
<div class='note-card-wrapper search-result-card-wrapper'>
|
|
101
|
+
<div class='note-card row-box search-result-card' onClick={() => onViewNote(note)}>
|
|
102
|
+
<div class='note-card-content flex-1'>
|
|
103
|
+
<div class='note-card-title'>{note.title}</div>
|
|
104
|
+
<div class='note-card-preview ellipsis'>{extractText(note.content)}</div>
|
|
105
|
+
<div class='note-card-date'>{new Date(note.updatedAt).toLocaleString()}</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const css: CssProps = {
|
|
115
|
+
height: '100%',
|
|
116
|
+
display: 'flex',
|
|
117
|
+
flexDirection: 'column',
|
|
118
|
+
overflowY: 'auto',
|
|
119
|
+
backgroundColor: 'var(--primary-bg-color)',
|
|
120
|
+
padding: '0 16px',
|
|
121
|
+
|
|
122
|
+
'.search-result-card-wrapper': {
|
|
123
|
+
marginBottom: '16px',
|
|
124
|
+
borderRadius: '12px',
|
|
125
|
+
overflow: 'hidden',
|
|
126
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.05)',
|
|
127
|
+
backgroundColor: 'var(--secondary-bg-color)',
|
|
128
|
+
},
|
|
129
|
+
'.search-result-card': {
|
|
130
|
+
backgroundColor: 'var(--primary-bg-color)',
|
|
131
|
+
borderRadius: '12px',
|
|
132
|
+
padding: '16px',
|
|
133
|
+
cursor: 'pointer',
|
|
134
|
+
alignItems: 'center',
|
|
135
|
+
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
|
136
|
+
'&:active': {
|
|
137
|
+
transform: 'scale(0.98)',
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
'.note-card-title': {
|
|
141
|
+
fontSize: '16px',
|
|
142
|
+
fontWeight: 'bold',
|
|
143
|
+
color: 'var(--primary-color)',
|
|
144
|
+
marginBottom: '4px',
|
|
145
|
+
},
|
|
146
|
+
'.note-card-preview': {
|
|
147
|
+
fontSize: '14px',
|
|
148
|
+
color: 'var(--secondary-color)',
|
|
149
|
+
marginBottom: '6px',
|
|
150
|
+
},
|
|
151
|
+
'.note-card-date': {
|
|
152
|
+
fontSize: '12px',
|
|
153
|
+
color: 'var(--secondary-color)',
|
|
154
|
+
opacity: 0.8,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const ref: RefProps = {
|
|
159
|
+
onLoad: async () => {
|
|
160
|
+
showHistory();
|
|
161
|
+
// focus input
|
|
162
|
+
const input = ref.$('.search-in-input') as HTMLInputElement;
|
|
163
|
+
if (input) {
|
|
164
|
+
requestAnimationFrame(() => input.focus());
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const headerTitle = (
|
|
170
|
+
<SearchInput
|
|
171
|
+
placeholder='Search title or content...'
|
|
172
|
+
onSearch={onSearch}
|
|
173
|
+
onClear={onClearSearch}
|
|
174
|
+
onFocus={showHistory}
|
|
175
|
+
class='w-100p'
|
|
176
|
+
hook={searchInputHook}
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<HeaderWithBackFrame
|
|
182
|
+
title={headerTitle}
|
|
183
|
+
right={<span></span>}
|
|
184
|
+
onBack={(event) => props.sliderFrameHook.close!(event)}
|
|
185
|
+
>
|
|
186
|
+
<div ref={ref} css={css} class='note-search-page'>
|
|
187
|
+
<SliderFrame hook={subSliderHook} />
|
|
188
|
+
{historyDom.node}
|
|
189
|
+
{listDom.node}
|
|
190
|
+
</div>
|
|
191
|
+
</HeaderWithBackFrame>
|
|
192
|
+
);
|
|
193
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { bindGlobalStyle, CssProps, RefProps } from 'lupine.components';
|
|
2
|
+
|
|
3
|
+
export interface SearchInputHookProps {
|
|
4
|
+
setValue?: (value: string) => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface InputWithClearProps {
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
onSearch: (value: string) => void;
|
|
10
|
+
onClear?: () => void;
|
|
11
|
+
onFocus?: () => void;
|
|
12
|
+
onBlur?: () => void;
|
|
13
|
+
class?: string;
|
|
14
|
+
searchIcon?: string;
|
|
15
|
+
hook?: SearchInputHookProps;
|
|
16
|
+
}
|
|
17
|
+
export const SearchInput = (props: InputWithClearProps) => {
|
|
18
|
+
const css: CssProps = {
|
|
19
|
+
display: 'flex',
|
|
20
|
+
flex: 1,
|
|
21
|
+
position: 'relative',
|
|
22
|
+
fontSize: '16px',
|
|
23
|
+
'.search-in-input': {
|
|
24
|
+
flex: 1,
|
|
25
|
+
paddingRight: '30px',
|
|
26
|
+
height: '32px',
|
|
27
|
+
padding: '4px 30px 4px 12px',
|
|
28
|
+
borderRadius: '16px',
|
|
29
|
+
border: 'none',
|
|
30
|
+
backgroundColor: 'var(--secondary-bg-color)',
|
|
31
|
+
color: 'var(--primary-color)',
|
|
32
|
+
outline: 'none',
|
|
33
|
+
},
|
|
34
|
+
'.search-in-search, .search-in-clear': {
|
|
35
|
+
position: 'absolute',
|
|
36
|
+
top: '50%',
|
|
37
|
+
transform: 'translateY(-50%)',
|
|
38
|
+
cursor: 'pointer',
|
|
39
|
+
display: 'flex',
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
justifyContent: 'center',
|
|
42
|
+
color: 'var(--secondary-color)',
|
|
43
|
+
},
|
|
44
|
+
'.search-in-search': {
|
|
45
|
+
right: '12px',
|
|
46
|
+
},
|
|
47
|
+
'.search-in-clear': {
|
|
48
|
+
display: 'none',
|
|
49
|
+
right: '34px',
|
|
50
|
+
},
|
|
51
|
+
'input.search-in-input:not(:placeholder-shown) + .search-in-clear': {
|
|
52
|
+
display: 'flex',
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
bindGlobalStyle('search-input-component', css);
|
|
56
|
+
|
|
57
|
+
const ref: RefProps = {};
|
|
58
|
+
const onSearch = () => {
|
|
59
|
+
const value = ref.$('.search-in-input').value;
|
|
60
|
+
props.onSearch(value);
|
|
61
|
+
};
|
|
62
|
+
const onClear = () => {
|
|
63
|
+
ref.$('.search-in-input').value = '';
|
|
64
|
+
props.onClear?.();
|
|
65
|
+
};
|
|
66
|
+
if (props.hook) {
|
|
67
|
+
props.hook.setValue = (value: string) => {
|
|
68
|
+
ref.$('input.search-in-input').value = value;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return (
|
|
72
|
+
<div ref={ref} class={'search-input-component ' + (props.class || '')}>
|
|
73
|
+
<input
|
|
74
|
+
type='text'
|
|
75
|
+
maxLength={30}
|
|
76
|
+
spellcheck={false}
|
|
77
|
+
class='search-in-input input-base'
|
|
78
|
+
placeholder={props.placeholder}
|
|
79
|
+
onKeyDown={(e) => {
|
|
80
|
+
if (e.key === 'Enter') {
|
|
81
|
+
onSearch();
|
|
82
|
+
}
|
|
83
|
+
}}
|
|
84
|
+
onBlur={() => props.onBlur?.()}
|
|
85
|
+
onFocus={() => props.onFocus?.()}
|
|
86
|
+
/>
|
|
87
|
+
<div class='search-in-clear' onClick={onClear}>
|
|
88
|
+
<i class='ifc-icon ma-close' />
|
|
89
|
+
</div>
|
|
90
|
+
<div class='search-in-search' onClick={onSearch}>
|
|
91
|
+
<i class={'ifc-icon ' + (props.searchIcon || 'bs-search')} />
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CssProps,
|
|
3
|
+
getGlobalStylesId,
|
|
4
|
+
bindGlobalStyle,
|
|
5
|
+
RefProps,
|
|
6
|
+
VNode,
|
|
7
|
+
MobileSideMenuHelper,
|
|
8
|
+
updateTheme,
|
|
9
|
+
} from 'lupine.components';
|
|
10
|
+
import { NotificationColor, NotificationMessage, SliderFrameHookProps } from 'lupine.components';
|
|
11
|
+
import { IconMenuItemProps } from 'lupine.components';
|
|
12
|
+
import { MineSettingsPage } from './mine-settings-page';
|
|
13
|
+
|
|
14
|
+
export type SideMenuContentProps = {
|
|
15
|
+
navItems?: IconMenuItemProps[];
|
|
16
|
+
menuItems?: string[];
|
|
17
|
+
title?: string;
|
|
18
|
+
footer?: string;
|
|
19
|
+
version?: string;
|
|
20
|
+
onAction?: (action: string) => void;
|
|
21
|
+
settingSliderHook: SliderFrameHookProps;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const sideMenuContentCss: CssProps = {
|
|
25
|
+
margin: '0 -12px',
|
|
26
|
+
'.msm-header': {
|
|
27
|
+
display: 'flex',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
padding: '16px',
|
|
30
|
+
borderBottom: '1px solid var(--primary-border-color)',
|
|
31
|
+
},
|
|
32
|
+
'.msm-header-icon': {
|
|
33
|
+
width: '32px',
|
|
34
|
+
height: '32px',
|
|
35
|
+
marginRight: '12px',
|
|
36
|
+
borderRadius: '4px',
|
|
37
|
+
backgroundColor: 'var(--primary-color)',
|
|
38
|
+
},
|
|
39
|
+
'.msm-header-title': {
|
|
40
|
+
fontSize: '18px',
|
|
41
|
+
fontWeight: 'bold',
|
|
42
|
+
color: 'var(--primary-color)',
|
|
43
|
+
},
|
|
44
|
+
'.msm-content': {
|
|
45
|
+
flex: '1',
|
|
46
|
+
overflowY: 'auto',
|
|
47
|
+
padding: '12px 0',
|
|
48
|
+
},
|
|
49
|
+
'.msm-item-link': {
|
|
50
|
+
textDecoration: 'none',
|
|
51
|
+
color: 'inherit',
|
|
52
|
+
display: 'block',
|
|
53
|
+
},
|
|
54
|
+
'.msm-separator': {
|
|
55
|
+
height: '1px',
|
|
56
|
+
backgroundColor: 'var(--primary-border-color)',
|
|
57
|
+
margin: '8px 12px',
|
|
58
|
+
},
|
|
59
|
+
'.msm-item': {
|
|
60
|
+
padding: '12px',
|
|
61
|
+
fontSize: '16px',
|
|
62
|
+
cursor: 'pointer',
|
|
63
|
+
transition: 'background-color 0.2s',
|
|
64
|
+
display: 'flex',
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
gap: '12px',
|
|
67
|
+
width: '100%',
|
|
68
|
+
'&:hover': {
|
|
69
|
+
backgroundColor: 'var(--activatable-bg-color-hover, rgba(0,0,0,0.04))',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
'.msm-footer': {
|
|
73
|
+
borderTop: '1px solid var(--primary-border-color)',
|
|
74
|
+
padding: '16px 0',
|
|
75
|
+
},
|
|
76
|
+
'.msm-footer-cfg': {
|
|
77
|
+
display: 'flex',
|
|
78
|
+
flexDirection: 'row',
|
|
79
|
+
justifyContent: 'space-between',
|
|
80
|
+
},
|
|
81
|
+
'.msm-footer-cfg .msm-item': {
|
|
82
|
+
padding: '12px',
|
|
83
|
+
},
|
|
84
|
+
'.msm-footer-version': {
|
|
85
|
+
textAlign: 'center',
|
|
86
|
+
fontSize: '12px',
|
|
87
|
+
color: 'var(--secondary-color, #999)',
|
|
88
|
+
marginTop: '2px',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const SideMenuContent = ({
|
|
93
|
+
navItems = [],
|
|
94
|
+
menuItems = ['Light Mode', 'Dark Mode'],
|
|
95
|
+
title = 'Function Menu',
|
|
96
|
+
footer,
|
|
97
|
+
onAction,
|
|
98
|
+
settingSliderHook,
|
|
99
|
+
}: SideMenuContentProps): VNode<any> => {
|
|
100
|
+
const globalCssId = getGlobalStylesId(sideMenuContentCss);
|
|
101
|
+
bindGlobalStyle(globalCssId, sideMenuContentCss);
|
|
102
|
+
|
|
103
|
+
const ref: RefProps = {
|
|
104
|
+
globalCssId,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleAction = (item: string) => {
|
|
108
|
+
if (onAction) {
|
|
109
|
+
onAction(item);
|
|
110
|
+
} else {
|
|
111
|
+
if (item === 'Light Mode') {
|
|
112
|
+
updateTheme('light');
|
|
113
|
+
MobileSideMenuHelper.hide();
|
|
114
|
+
} else if (item === 'Dark Mode') {
|
|
115
|
+
updateTheme('dark');
|
|
116
|
+
MobileSideMenuHelper.hide();
|
|
117
|
+
} else {
|
|
118
|
+
NotificationMessage.sendMessage(`Action: ${item}`, NotificationColor.Info);
|
|
119
|
+
MobileSideMenuHelper.hide();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div ref={ref} style={{ display: 'flex', flexDirection: 'column', flex: 1, height: '100%' }}>
|
|
126
|
+
<div class='msm-header'>
|
|
127
|
+
{/* <div class='msm-header-icon'></div> */}
|
|
128
|
+
<div class='msm-header-title'>{title}</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class='msm-content'>
|
|
132
|
+
{navItems &&
|
|
133
|
+
navItems.map((item, index) => {
|
|
134
|
+
const handleClick = (e: Event) => {
|
|
135
|
+
if (item.js) {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
item.js();
|
|
138
|
+
}
|
|
139
|
+
MobileSideMenuHelper.hide();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<a href={item.url || 'javascript:void(0)'} class='msm-item-link' onClick={handleClick}>
|
|
144
|
+
<div class='msm-item'>
|
|
145
|
+
{item.icon && <i class={`ifc-icon ${item.icon}`}></i>}
|
|
146
|
+
{item.text}
|
|
147
|
+
</div>
|
|
148
|
+
</a>
|
|
149
|
+
);
|
|
150
|
+
})}
|
|
151
|
+
{navItems && navItems.length > 0 && menuItems.length > 0 && <div class='msm-separator'></div>}
|
|
152
|
+
{menuItems.map((item) => (
|
|
153
|
+
<div class='msm-item' onClick={() => handleAction(item)}>
|
|
154
|
+
{item}
|
|
155
|
+
</div>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div class='msm-footer'>
|
|
160
|
+
<div class='msm-footer-cfg'>
|
|
161
|
+
<div
|
|
162
|
+
class='msm-item'
|
|
163
|
+
onClick={() => {
|
|
164
|
+
settingSliderHook.load!(
|
|
165
|
+
<MineSettingsPage sliderFrameHook={settingSliderHook} onDataChanged={() => {}} />
|
|
166
|
+
);
|
|
167
|
+
MobileSideMenuHelper.hide();
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
<i class='ifc-icon ma-cog-outline'></i>
|
|
171
|
+
Settings
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
{footer && <div class='msm-footer-version'>{footer}</div>}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { VNode } from 'lupine.web';
|
|
2
|
+
import { MediaQueryMaxWidth, ResponsiveFrame, SliderFrame, SliderFrameHookProps } from 'lupine.components';
|
|
3
|
+
import { SideMenuContent } from '../components/side-menu-content';
|
|
4
|
+
|
|
5
|
+
// Note: Replace with true site config loading if available
|
|
6
|
+
const getSiteTitle = async () => 'My Note App';
|
|
7
|
+
const getSiteFooter = async () => '© 2026 My Note App';
|
|
8
|
+
|
|
9
|
+
// 'sidemenu' | 'tabs', this will be replaced by create-lupine
|
|
10
|
+
const DEFAULT_LAYOUT = 'tabs';
|
|
11
|
+
export const AppResponsiveFrame = async (placeholderClassname: string, vnode: VNode<any>) => {
|
|
12
|
+
const mobileBottomMenu = [
|
|
13
|
+
{ icon: 'ma-home-outline', url: '/', text: 'Notes' },
|
|
14
|
+
{ icon: 'icon-finance', url: '/finance', text: 'Finance' },
|
|
15
|
+
{ icon: 'ma-book-outline', url: '/about', text: 'About', topout: true },
|
|
16
|
+
{ icon: 'ma-tools', url: '/tools', text: 'Tools' },
|
|
17
|
+
{ icon: 'ma-account-cog-outline', url: '/mine', text: 'Mine' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const layout: 'sidemenu' | 'tabs' = (localStorage.getItem('app-layout') as any) || DEFAULT_LAYOUT;
|
|
21
|
+
const sliderFrameHook: SliderFrameHookProps = {};
|
|
22
|
+
|
|
23
|
+
return ResponsiveFrame({
|
|
24
|
+
placeholderClassname,
|
|
25
|
+
mainContent: vnode,
|
|
26
|
+
desktopHeaderTitle: await getSiteTitle(),
|
|
27
|
+
desktopFooterTitle: await getSiteFooter(),
|
|
28
|
+
desktopTopMenu: layout === 'sidemenu' ? undefined : mobileBottomMenu,
|
|
29
|
+
mobileBottomMenu: layout === 'sidemenu' ? undefined : mobileBottomMenu,
|
|
30
|
+
sharedContents: (
|
|
31
|
+
<>
|
|
32
|
+
<SliderFrame hook={sliderFrameHook} />
|
|
33
|
+
</>
|
|
34
|
+
),
|
|
35
|
+
mobileSideMenuContent: (
|
|
36
|
+
<SideMenuContent
|
|
37
|
+
navItems={layout === 'sidemenu' ? mobileBottomMenu : undefined}
|
|
38
|
+
settingSliderHook={sliderFrameHook}
|
|
39
|
+
title='My Note App'
|
|
40
|
+
footer='Powered by Lupine.js'
|
|
41
|
+
/>
|
|
42
|
+
),
|
|
43
|
+
maxWidth: MediaQueryMaxWidth.DesktopMax,
|
|
44
|
+
autoExtendSidemenu: layout === 'sidemenu',
|
|
45
|
+
});
|
|
46
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html data-theme="<!--META-THEME-->">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title><!--META-TITLE--></title>
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=, user-scalable=no" />
|
|
7
|
+
<!--META-ENV-START-->
|
|
8
|
+
<!--META-ENV-END-->
|
|
9
|
+
<link rel="shortcut icon" href="{SUBDIR}/assets/favicon.ico?t={hash}" type="image/x-icon" />
|
|
10
|
+
<link rel="stylesheet" type="text/css" href="{SUBDIR}/index.css?t={hash}" />
|
|
11
|
+
<script defer src="{SUBDIR}/index.js#t={hash}"></script>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div class="lupine-root"></div>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// css order is important
|
|
2
|
+
import './styles/global.css';
|
|
3
|
+
import './styles/app.css';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
bindRouter,
|
|
7
|
+
bindGlobalStyle,
|
|
8
|
+
debugWatch,
|
|
9
|
+
isFrontEnd,
|
|
10
|
+
PageRouter,
|
|
11
|
+
webEnv,
|
|
12
|
+
bindLang,
|
|
13
|
+
bindTheme,
|
|
14
|
+
setDefaultPageTitle,
|
|
15
|
+
} from 'lupine.components';
|
|
16
|
+
import { baseCss } from './styles/base-css';
|
|
17
|
+
import { themes } from './styles/theme';
|
|
18
|
+
import { AppResponsiveFrame } from './frames/app-responsive-frame';
|
|
19
|
+
import { HomePage } from './pages/home-page';
|
|
20
|
+
import { FinancePage } from './pages/finance-page';
|
|
21
|
+
import { AboutPage } from './pages/about-page';
|
|
22
|
+
import { MinePage } from './pages/mine-page';
|
|
23
|
+
import { ToolsPage } from './pages/tools-page';
|
|
24
|
+
|
|
25
|
+
bindLang('zh-cn', {});
|
|
26
|
+
bindTheme('light', themes);
|
|
27
|
+
bindGlobalStyle('comm-css', baseCss, false, true);
|
|
28
|
+
setDefaultPageTitle('Lupine.js Note Starter');
|
|
29
|
+
|
|
30
|
+
if (isFrontEnd() && webEnv('NODE_ENV', '') === 'development') {
|
|
31
|
+
debugWatch(webEnv('API_PORT', 0));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const pageRouter = new PageRouter();
|
|
35
|
+
pageRouter.setFramePage({ component: AppResponsiveFrame, placeholderClassname: 'user-page-placeholder' });
|
|
36
|
+
pageRouter.use('/finance', FinancePage);
|
|
37
|
+
pageRouter.use('/about', AboutPage);
|
|
38
|
+
pageRouter.use('/mine', MinePage);
|
|
39
|
+
pageRouter.use('/tools', ToolsPage);
|
|
40
|
+
pageRouter.use('*', HomePage);
|
|
41
|
+
|
|
42
|
+
bindRouter(pageRouter);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CssProps,
|
|
3
|
+
MobileHeaderCenter,
|
|
4
|
+
MobileHeaderEmptyIcon,
|
|
5
|
+
MobileHeaderTitleIcon,
|
|
6
|
+
MobileTopSysIcon,
|
|
7
|
+
PageProps,
|
|
8
|
+
RefProps,
|
|
9
|
+
} from 'lupine.components';
|
|
10
|
+
import { MineAboutContent } from '../components/mine-about-page';
|
|
11
|
+
|
|
12
|
+
export const AboutPage = async (props: PageProps) => {
|
|
13
|
+
const css: CssProps = {
|
|
14
|
+
display: 'flex',
|
|
15
|
+
flexDirection: 'column',
|
|
16
|
+
height: '100%',
|
|
17
|
+
position: 'relative',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const ref: RefProps = {
|
|
21
|
+
onLoad: async () => {},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div css={css} ref={ref}>
|
|
26
|
+
<MobileHeaderCenter>
|
|
27
|
+
<MobileHeaderTitleIcon
|
|
28
|
+
title='About'
|
|
29
|
+
left={<MobileHeaderEmptyIcon />}
|
|
30
|
+
right={
|
|
31
|
+
<div class='flex-center-gap-12'>
|
|
32
|
+
<MobileTopSysIcon />
|
|
33
|
+
</div>
|
|
34
|
+
}
|
|
35
|
+
/>
|
|
36
|
+
</MobileHeaderCenter>
|
|
37
|
+
|
|
38
|
+
<div class='tools-scroll tools-list-container no-scrollbar-container'>
|
|
39
|
+
<MineAboutContent />
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CssProps,
|
|
3
|
+
MobileHeaderCenter,
|
|
4
|
+
MobileHeaderEmptyIcon,
|
|
5
|
+
MobileHeaderTitleIcon,
|
|
6
|
+
MobileTopSysIcon,
|
|
7
|
+
PageProps,
|
|
8
|
+
RefProps,
|
|
9
|
+
} from 'lupine.components';
|
|
10
|
+
|
|
11
|
+
export const FinancePage = async (props: PageProps) => {
|
|
12
|
+
const css: CssProps = {
|
|
13
|
+
display: 'flex',
|
|
14
|
+
flexDirection: 'column',
|
|
15
|
+
height: '100%',
|
|
16
|
+
position: 'relative',
|
|
17
|
+
'.finance-list-container': {
|
|
18
|
+
padding: '32px 24px',
|
|
19
|
+
display: 'flex',
|
|
20
|
+
flexDirection: 'column',
|
|
21
|
+
alignItems: 'center',
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const ref: RefProps = {
|
|
26
|
+
onLoad: async () => {},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div css={css} ref={ref}>
|
|
31
|
+
<MobileHeaderCenter>
|
|
32
|
+
<MobileHeaderTitleIcon
|
|
33
|
+
title='Finance'
|
|
34
|
+
left={<MobileHeaderEmptyIcon />}
|
|
35
|
+
right={
|
|
36
|
+
<div class='flex-center-gap-12'>
|
|
37
|
+
<MobileTopSysIcon />
|
|
38
|
+
</div>
|
|
39
|
+
}
|
|
40
|
+
/>
|
|
41
|
+
</MobileHeaderCenter>
|
|
42
|
+
|
|
43
|
+
<div class='finance-list-container no-scrollbar-container'>This is a finance demo page.</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
};
|