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,219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActionSheetSelect,
|
|
3
|
+
CssProps,
|
|
4
|
+
HeaderWithBackFrame,
|
|
5
|
+
HtmlVar,
|
|
6
|
+
NotificationColor,
|
|
7
|
+
NotificationMessage,
|
|
8
|
+
RefProps,
|
|
9
|
+
SliderFrame,
|
|
10
|
+
SliderFrameHookProps,
|
|
11
|
+
ToggleSwitch,
|
|
12
|
+
ToggleSwitchSize,
|
|
13
|
+
} from 'lupine.components';
|
|
14
|
+
import { MineService } from '../services/mine-service';
|
|
15
|
+
import { MineAboutPage } from './mine-about-page';
|
|
16
|
+
|
|
17
|
+
export const MineSettingsPage = (props: { sliderFrameHook: SliderFrameHookProps; onDataChanged: () => void }) => {
|
|
18
|
+
const innerSliderHook: SliderFrameHookProps = {};
|
|
19
|
+
|
|
20
|
+
const onClearAll = async () => {
|
|
21
|
+
await ActionSheetSelect.show({
|
|
22
|
+
title: 'DANGER: Are you sure you want to permanently delete ALL local data? This cannot be undone.',
|
|
23
|
+
options: ['Erase Everything'],
|
|
24
|
+
cancelButtonText: 'Cancel',
|
|
25
|
+
handleClicked: async (index: number, close: () => void) => {
|
|
26
|
+
close();
|
|
27
|
+
if (index === 0) {
|
|
28
|
+
MineService.clearAllData();
|
|
29
|
+
props.onDataChanged();
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const onClearCache = async () => {
|
|
36
|
+
// Fake mock loading for UX
|
|
37
|
+
const loader = document.createElement('div');
|
|
38
|
+
loader.style.position = 'fixed';
|
|
39
|
+
loader.style.top = '50%';
|
|
40
|
+
loader.style.left = '50%';
|
|
41
|
+
loader.style.transform = 'translate(-50%, -50%)';
|
|
42
|
+
loader.style.background = 'rgba(0,0,0,0.8)';
|
|
43
|
+
loader.style.color = '#fff';
|
|
44
|
+
loader.style.padding = '16px 24px';
|
|
45
|
+
loader.style.borderRadius = '8px';
|
|
46
|
+
loader.style.zIndex = '9999';
|
|
47
|
+
loader.innerText = 'Clearing cache...';
|
|
48
|
+
document.body.appendChild(loader);
|
|
49
|
+
|
|
50
|
+
setTimeout(() => {
|
|
51
|
+
document.body.removeChild(loader);
|
|
52
|
+
NotificationMessage.sendMessage('Cache cleared.', NotificationColor.Success);
|
|
53
|
+
}, 800);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const renderContent = () => {
|
|
57
|
+
const css: CssProps = {
|
|
58
|
+
display: 'flex',
|
|
59
|
+
flexDirection: 'column',
|
|
60
|
+
flex: '1',
|
|
61
|
+
padding: '0 16px',
|
|
62
|
+
backgroundColor: 'var(--secondary-bg-color)',
|
|
63
|
+
overflowY: 'auto',
|
|
64
|
+
|
|
65
|
+
'.setting-section-group': {
|
|
66
|
+
marginBottom: '24px',
|
|
67
|
+
},
|
|
68
|
+
'.setting-section-title': {
|
|
69
|
+
fontSize: '14px',
|
|
70
|
+
color: 'var(--secondary-color)',
|
|
71
|
+
marginBottom: '8px',
|
|
72
|
+
paddingLeft: '16px',
|
|
73
|
+
marginTop: '16px',
|
|
74
|
+
},
|
|
75
|
+
'.setting-section-block': {
|
|
76
|
+
backgroundColor: 'var(--primary-bg-color)',
|
|
77
|
+
borderRadius: '12px',
|
|
78
|
+
overflow: 'hidden',
|
|
79
|
+
},
|
|
80
|
+
'.setting-section-item': {
|
|
81
|
+
display: 'flex',
|
|
82
|
+
alignItems: 'center',
|
|
83
|
+
justifyContent: 'space-between',
|
|
84
|
+
padding: '16px',
|
|
85
|
+
borderBottom: '1px solid var(--primary-border-color)',
|
|
86
|
+
cursor: 'pointer',
|
|
87
|
+
transition: 'background-color 0.2s',
|
|
88
|
+
},
|
|
89
|
+
'.setting-section-item:last-child': {
|
|
90
|
+
borderBottom: 'none',
|
|
91
|
+
},
|
|
92
|
+
'.setting-section-item:active': {
|
|
93
|
+
backgroundColor: 'rgba(0,0,0,0.02)',
|
|
94
|
+
},
|
|
95
|
+
'.setting-section-item-text': {
|
|
96
|
+
fontSize: '16px',
|
|
97
|
+
color: 'var(--primary-color)',
|
|
98
|
+
},
|
|
99
|
+
'.setting-section-item-text.danger': {
|
|
100
|
+
color: '#ff4d4f',
|
|
101
|
+
},
|
|
102
|
+
'.setting-section-item-icon': {
|
|
103
|
+
color: 'var(--secondary-color)',
|
|
104
|
+
opacity: 0.5,
|
|
105
|
+
},
|
|
106
|
+
'a.setting-section-item': {
|
|
107
|
+
textDecoration: 'none',
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const ref: RefProps = {
|
|
112
|
+
onLoad: async (el: Element) => {
|
|
113
|
+
// any sub loaders
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div css={css} ref={ref} class='user-settings-top'>
|
|
119
|
+
<div class='setting-section-group'>
|
|
120
|
+
<div class='setting-section-title'>App Layout</div>
|
|
121
|
+
<div class='setting-section-block'>
|
|
122
|
+
<div class='setting-section-item'>
|
|
123
|
+
<div class='setting-section-item-text'>
|
|
124
|
+
<div style={{ fontWeight: 'bold' }}>Desktop Menu</div>
|
|
125
|
+
<div style={{ fontSize: '14px', color: 'var(--secondary-color)', marginTop: '4px' }}>
|
|
126
|
+
{(localStorage.getItem('app-layout') || 'tabs') === 'sidemenu' ? 'Sidebar Menu' : 'Bottom Tabs'}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<div class='setting-section-item-icon' style={{ opacity: 1 }}>
|
|
130
|
+
<ToggleSwitch
|
|
131
|
+
size={ToggleSwitchSize.Medium}
|
|
132
|
+
checked={(localStorage.getItem('app-layout') || 'tabs') === 'sidemenu'}
|
|
133
|
+
disabled={false}
|
|
134
|
+
onClick={async (v) => {
|
|
135
|
+
const newLayout = v ? 'sidemenu' : 'tabs';
|
|
136
|
+
localStorage.setItem('app-layout', newLayout);
|
|
137
|
+
dom.value = renderContent(); // Re-render first
|
|
138
|
+
|
|
139
|
+
await ActionSheetSelect.show({
|
|
140
|
+
title: 'Layout changed. Restart app to apply?',
|
|
141
|
+
options: ['Restart Now'],
|
|
142
|
+
cancelButtonText: 'Later',
|
|
143
|
+
handleClicked: async (index: number, close: () => void) => {
|
|
144
|
+
close();
|
|
145
|
+
if (index === 0) {
|
|
146
|
+
window.location.href = '/';
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div class='setting-section-group'>
|
|
158
|
+
<div class='setting-section-title'>Application</div>
|
|
159
|
+
<div class='setting-section-block'>
|
|
160
|
+
<a
|
|
161
|
+
class='setting-section-item'
|
|
162
|
+
href='javascript:void(0)'
|
|
163
|
+
onClick={() => {
|
|
164
|
+
innerSliderHook.load!(<MineAboutPage sliderFrameHook={innerSliderHook} />);
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
<div class='setting-section-item-text'>Feedback & Support</div>
|
|
168
|
+
<div class='setting-section-item-icon'>
|
|
169
|
+
<i class='ifc-icon ma-chevron-right' />
|
|
170
|
+
</div>
|
|
171
|
+
</a>
|
|
172
|
+
<div class='setting-section-item' onClick={onClearCache}>
|
|
173
|
+
<div class='setting-section-item-text'>Clear Temporary Cache</div>
|
|
174
|
+
<div class='setting-section-item-icon'>
|
|
175
|
+
<i class='ifc-icon ma-chevron-right' />
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
<div
|
|
179
|
+
class='setting-section-item'
|
|
180
|
+
onClick={() => {
|
|
181
|
+
// props.sliderFrameHook.addClass!('desktop-slide-left');
|
|
182
|
+
// innerSliderHook.addClass!('desktop-slide-right');
|
|
183
|
+
innerSliderHook.load!(<MineAboutPage sliderFrameHook={innerSliderHook} />);
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
<div class='setting-section-item-text'>About</div>
|
|
187
|
+
<div class='setting-section-item-icon'>
|
|
188
|
+
<i class='ifc-icon ma-chevron-right' />
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div class='setting-section-group'>
|
|
195
|
+
<div class='setting-section-title text-danger'>Danger Zone</div>
|
|
196
|
+
<div class='setting-section-block'>
|
|
197
|
+
<div class='setting-section-item' onClick={onClearAll}>
|
|
198
|
+
<div class='setting-section-item-text danger'>Clear All Application Data</div>
|
|
199
|
+
<div class='setting-section-item-icon'>
|
|
200
|
+
<i class='ifc-icon ma-delete-outline text-danger' />
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const dom = new HtmlVar(renderContent());
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<HeaderWithBackFrame title='Settings' onBack={(event) => props.sliderFrameHook.close!(event)}>
|
|
213
|
+
<>
|
|
214
|
+
<SliderFrame hook={innerSliderHook} />
|
|
215
|
+
{dom.node}
|
|
216
|
+
</>
|
|
217
|
+
</HeaderWithBackFrame>
|
|
218
|
+
);
|
|
219
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { CssProps, RefProps } from 'lupine.components';
|
|
2
|
+
import { MobileTopSysIcon } from 'lupine.components';
|
|
3
|
+
|
|
4
|
+
// Helper hook pattern to pass search calls back to the Homepage
|
|
5
|
+
export const NoteSearchHelper: {
|
|
6
|
+
triggerSearch?: (query: string) => void;
|
|
7
|
+
} = {};
|
|
8
|
+
|
|
9
|
+
export const MobileTopSearchMenu = () => {
|
|
10
|
+
const css: CssProps = {
|
|
11
|
+
display: 'flex',
|
|
12
|
+
flexDirection: 'row',
|
|
13
|
+
width: '100%',
|
|
14
|
+
alignItems: 'center',
|
|
15
|
+
|
|
16
|
+
'.mobile-top-menu-left': {
|
|
17
|
+
display: 'flex',
|
|
18
|
+
flexDirection: 'row',
|
|
19
|
+
flex: 1,
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
fontSize: '18px',
|
|
22
|
+
fontWeight: 'bold',
|
|
23
|
+
color: 'var(--primary-color)',
|
|
24
|
+
},
|
|
25
|
+
'.mobile-top-menu-right': {
|
|
26
|
+
display: 'flex',
|
|
27
|
+
flexDirection: 'row',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
gap: '12px',
|
|
30
|
+
},
|
|
31
|
+
'.mtm-input-group': {
|
|
32
|
+
display: 'none',
|
|
33
|
+
flex: 1,
|
|
34
|
+
marginRight: '12px',
|
|
35
|
+
},
|
|
36
|
+
'.mtm-input-box': {
|
|
37
|
+
width: '100%',
|
|
38
|
+
padding: '4px 8px',
|
|
39
|
+
borderRadius: '4px',
|
|
40
|
+
border: '1px solid var(--secondary-border-color)',
|
|
41
|
+
backgroundColor: 'var(--primary-bg-color)',
|
|
42
|
+
color: 'var(--primary-color)',
|
|
43
|
+
},
|
|
44
|
+
'.mtm-search-icon, .mtm-menu-icon, .mtm-search-cancel': {
|
|
45
|
+
cursor: 'pointer',
|
|
46
|
+
display: 'flex',
|
|
47
|
+
},
|
|
48
|
+
'.mtm-search-cancel': {
|
|
49
|
+
display: 'none',
|
|
50
|
+
fontSize: '14px',
|
|
51
|
+
color: 'var(--primary-color)',
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Toggle state specific styles
|
|
55
|
+
'&.show-search': {
|
|
56
|
+
'.mobile-title': { display: 'none' },
|
|
57
|
+
'.mtm-search-icon': { display: 'none' },
|
|
58
|
+
'.mtm-input-group': { display: 'flex' },
|
|
59
|
+
'.mtm-search-cancel': { display: 'flex' },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const ref: RefProps = {
|
|
64
|
+
onLoad: async () => {
|
|
65
|
+
ref.$('.mtm-input-box')?.addEventListener('keyup', (e: Event) => {
|
|
66
|
+
if ((e as KeyboardEvent).key === 'Enter') {
|
|
67
|
+
onStartSearch();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const toggleSearchMode = () => {
|
|
74
|
+
const root = ref.current;
|
|
75
|
+
if (root) {
|
|
76
|
+
const isSearchMode = root.classList.toggle('show-search');
|
|
77
|
+
if (isSearchMode) {
|
|
78
|
+
(ref.$('.mtm-input-box') as HTMLElement)?.focus();
|
|
79
|
+
} else {
|
|
80
|
+
// Clear search when canceling
|
|
81
|
+
(ref.$('.mtm-input-box') as HTMLInputElement).value = '';
|
|
82
|
+
NoteSearchHelper.triggerSearch?.('');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const onStartSearch = () => {
|
|
88
|
+
const val = (ref.$('.mtm-input-box') as HTMLInputElement).value;
|
|
89
|
+
NoteSearchHelper.triggerSearch?.(val);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div ref={ref} css={css} class='mobile-top-menu-box'>
|
|
94
|
+
<div class='mobile-top-menu-left'>
|
|
95
|
+
<div class='mobile-title'>Notes</div>
|
|
96
|
+
<div class='mtm-input-group'>
|
|
97
|
+
<input
|
|
98
|
+
type='text'
|
|
99
|
+
spellcheck={false}
|
|
100
|
+
class='mtm-input-box'
|
|
101
|
+
placeholder='Search notes...'
|
|
102
|
+
autocomplete='off'
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
<div class='mobile-top-menu-right'>
|
|
107
|
+
<div class='mtm-search-cancel' onClick={toggleSearchMode}>
|
|
108
|
+
Cancel
|
|
109
|
+
</div>
|
|
110
|
+
<div class='mtm-search-icon' onClick={toggleSearchMode}>
|
|
111
|
+
<i class='ifc-icon bs-search'></i>
|
|
112
|
+
</div>
|
|
113
|
+
<div class='mtm-menu-icon'>
|
|
114
|
+
<MobileTopSysIcon />
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CssProps,
|
|
3
|
+
PageProps,
|
|
4
|
+
RefProps,
|
|
5
|
+
SliderFrameHookProps,
|
|
6
|
+
HeaderWithBackFrame,
|
|
7
|
+
ActionSheetSelect,
|
|
8
|
+
} from 'lupine.components';
|
|
9
|
+
import { LocalNoteProps, LocalNotesService } from '../services/local-notes-service';
|
|
10
|
+
import { NoteEditComponent } from './note-edit';
|
|
11
|
+
|
|
12
|
+
interface NoteDetailProps {
|
|
13
|
+
note: LocalNoteProps;
|
|
14
|
+
sliderFrameHook: SliderFrameHookProps;
|
|
15
|
+
onSaved: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const NoteDetailComponent = async (props: NoteDetailProps) => {
|
|
19
|
+
const css: CssProps = {
|
|
20
|
+
flex: 1,
|
|
21
|
+
overflowY: 'auto',
|
|
22
|
+
padding: '24px',
|
|
23
|
+
backgroundColor: 'var(--primary-bg-color)',
|
|
24
|
+
|
|
25
|
+
'.note-detail-title': {
|
|
26
|
+
fontSize: '24px',
|
|
27
|
+
fontWeight: 'bold',
|
|
28
|
+
color: 'var(--primary-color)',
|
|
29
|
+
marginBottom: '12px',
|
|
30
|
+
lineHeight: '1.4',
|
|
31
|
+
},
|
|
32
|
+
'.note-detail-date': {
|
|
33
|
+
fontSize: '13px',
|
|
34
|
+
color: 'var(--secondary-color)',
|
|
35
|
+
},
|
|
36
|
+
'.note-detail-meta': {
|
|
37
|
+
display: 'flex',
|
|
38
|
+
gap: '12px',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
marginBottom: '24px',
|
|
41
|
+
paddingBottom: '16px',
|
|
42
|
+
borderBottom: '1px solid var(--primary-border-color)',
|
|
43
|
+
},
|
|
44
|
+
'.note-detail-content': {
|
|
45
|
+
fontSize: '16px',
|
|
46
|
+
color: 'var(--primary-color)',
|
|
47
|
+
lineHeight: '1.6',
|
|
48
|
+
whiteSpace: 'pre-wrap',
|
|
49
|
+
wordBreak: 'break-word',
|
|
50
|
+
},
|
|
51
|
+
'.header-action-icon': {
|
|
52
|
+
fontSize: '20px',
|
|
53
|
+
padding: '4px',
|
|
54
|
+
cursor: 'pointer',
|
|
55
|
+
color: 'var(--primary-color)',
|
|
56
|
+
},
|
|
57
|
+
'.header-actions-container': {
|
|
58
|
+
display: 'flex',
|
|
59
|
+
gap: '16px',
|
|
60
|
+
alignItems: 'center',
|
|
61
|
+
},
|
|
62
|
+
'.header-action-delete': {
|
|
63
|
+
color: '#ff4d4f',
|
|
64
|
+
},
|
|
65
|
+
'.note-detail-color-marker': {
|
|
66
|
+
width: '16px',
|
|
67
|
+
height: '16px',
|
|
68
|
+
borderRadius: '4px',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const onEdit = () => {
|
|
73
|
+
// Open edit component and pass the current note
|
|
74
|
+
props.sliderFrameHook.load!(
|
|
75
|
+
<NoteEditComponent note={props.note} sliderFrameHook={props.sliderFrameHook} onSaved={props.onSaved} />
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const onDelete = async () => {
|
|
80
|
+
await ActionSheetSelect.show({
|
|
81
|
+
title: 'Are you sure you want to delete this note?',
|
|
82
|
+
options: ['Remove'],
|
|
83
|
+
cancelButtonText: 'Cancel',
|
|
84
|
+
handleClicked: async (index: number, close: () => void) => {
|
|
85
|
+
close();
|
|
86
|
+
if (index === 0) {
|
|
87
|
+
LocalNotesService.deleteNote(props.note.id);
|
|
88
|
+
props.onSaved(); // trigger list refresh
|
|
89
|
+
props.sliderFrameHook.close!(new MouseEvent('click')); // close detail view
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const pageRight = (
|
|
96
|
+
<div class='header-actions-container'>
|
|
97
|
+
<i class='ifc-icon ma-pencil-outline header-action-icon' onClick={onEdit}></i>
|
|
98
|
+
<i class='ifc-icon ma-delete-off-outline header-action-icon header-action-delete' onClick={onDelete}></i>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<HeaderWithBackFrame title='Details' onBack={(e: Event) => props.sliderFrameHook.close!(e)} right={pageRight}>
|
|
104
|
+
<div css={css} class='note-detail-wrapper no-scrollbar-container flex-col h-100'>
|
|
105
|
+
<div class='note-detail-title'>{props.note.title}</div>
|
|
106
|
+
<div class='note-detail-meta'>
|
|
107
|
+
{props.note.color && (
|
|
108
|
+
<div class='note-detail-color-marker' style={{ backgroundColor: props.note.color }}></div>
|
|
109
|
+
)}
|
|
110
|
+
<div class='note-detail-date'>{new Date(props.note.updatedAt).toLocaleString()}</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div class='note-detail-content'>{props.note.content}</div>
|
|
113
|
+
</div>
|
|
114
|
+
</HeaderWithBackFrame>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CssProps,
|
|
3
|
+
NotificationColor,
|
|
4
|
+
NotificationMessage,
|
|
5
|
+
RefProps,
|
|
6
|
+
SliderFrameHookProps,
|
|
7
|
+
HeaderWithBackFrame,
|
|
8
|
+
ActionSheetColorPicker,
|
|
9
|
+
} from 'lupine.components';
|
|
10
|
+
// HEditor relies on complex setup in original app, fallback to simple textarea for robustness here
|
|
11
|
+
import { LocalNoteProps, LocalNotesService } from '../services/local-notes-service';
|
|
12
|
+
|
|
13
|
+
export const NoteEditComponent = (props: {
|
|
14
|
+
note?: LocalNoteProps;
|
|
15
|
+
sliderFrameHook: SliderFrameHookProps;
|
|
16
|
+
onSaved: () => void;
|
|
17
|
+
}) => {
|
|
18
|
+
const isEdit = !!props.note;
|
|
19
|
+
const noteId = props.note ? props.note.id : -1;
|
|
20
|
+
const defaultTitle = props.note ? props.note.title : '';
|
|
21
|
+
const defaultContent = props.note ? props.note.content : '';
|
|
22
|
+
|
|
23
|
+
const ref: RefProps = {
|
|
24
|
+
onLoad: async () => {
|
|
25
|
+
// Auto-focus title if new note
|
|
26
|
+
if (!isEdit) {
|
|
27
|
+
const tInput = ref.$('.note-edit-title-input') as HTMLInputElement;
|
|
28
|
+
tInput?.focus();
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let selectedColor = props.note?.color || '';
|
|
34
|
+
|
|
35
|
+
const pickColor = async () => {
|
|
36
|
+
const res = await ActionSheetColorPicker({
|
|
37
|
+
value: selectedColor,
|
|
38
|
+
title: 'Select Color Tag',
|
|
39
|
+
});
|
|
40
|
+
if (res !== undefined) {
|
|
41
|
+
selectedColor = res;
|
|
42
|
+
const ind = ref.$('.note-color-preview') as HTMLDivElement;
|
|
43
|
+
if (ind) ind.style.backgroundColor = res || 'transparent';
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const onSave = () => {
|
|
48
|
+
const title = (ref.$('.note-edit-title-input') as HTMLInputElement).value.trim();
|
|
49
|
+
const content = (ref.$('.note-edit-body-input') as HTMLTextAreaElement).value.trim();
|
|
50
|
+
|
|
51
|
+
if (!title) {
|
|
52
|
+
NotificationMessage.sendMessage('Please enter a note title', NotificationColor.Error);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
LocalNotesService.saveNote({
|
|
57
|
+
id: noteId,
|
|
58
|
+
title,
|
|
59
|
+
content,
|
|
60
|
+
color: selectedColor,
|
|
61
|
+
updatedAt: Date.now(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
NotificationMessage.sendMessage(isEdit ? 'Modified and saved' : 'Note added', NotificationColor.Success);
|
|
65
|
+
props.onSaved();
|
|
66
|
+
props.sliderFrameHook.close!(new MouseEvent('click'));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const css: CssProps = {
|
|
70
|
+
display: 'flex',
|
|
71
|
+
flexDirection: 'column',
|
|
72
|
+
height: '100%',
|
|
73
|
+
backgroundColor: 'var(--primary-bg-color)',
|
|
74
|
+
padding: '16px',
|
|
75
|
+
|
|
76
|
+
'.note-edit-title-input': {
|
|
77
|
+
width: '100%',
|
|
78
|
+
fontSize: '20px',
|
|
79
|
+
fontWeight: 'bold',
|
|
80
|
+
padding: '8px',
|
|
81
|
+
backgroundColor: 'transparent',
|
|
82
|
+
color: 'var(--primary-color)',
|
|
83
|
+
outline: 'none',
|
|
84
|
+
marginBottom: '16px',
|
|
85
|
+
'&:focus': {
|
|
86
|
+
borderBottomColor: 'var(--primary-accent-color)',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
'.note-edit-body-input': {
|
|
90
|
+
flex: 1,
|
|
91
|
+
padding: '12px',
|
|
92
|
+
backgroundColor: 'transparent',
|
|
93
|
+
color: 'var(--primary-color)',
|
|
94
|
+
fontSize: '16px',
|
|
95
|
+
resize: 'none',
|
|
96
|
+
outline: 'none',
|
|
97
|
+
lineHeight: '1.6',
|
|
98
|
+
},
|
|
99
|
+
'.note-edit-color-row': {
|
|
100
|
+
display: 'flex',
|
|
101
|
+
alignItems: 'center',
|
|
102
|
+
padding: '12px',
|
|
103
|
+
cursor: 'pointer',
|
|
104
|
+
backgroundColor: 'var(--secondary-bg-color)',
|
|
105
|
+
borderRadius: '8px',
|
|
106
|
+
marginBottom: '16px',
|
|
107
|
+
color: 'var(--primary-color)',
|
|
108
|
+
fontWeight: 'bold',
|
|
109
|
+
fontSize: '16px',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<HeaderWithBackFrame
|
|
115
|
+
title={isEdit ? 'Edit Note' : 'New Note'}
|
|
116
|
+
onBack={(e: Event) => props.sliderFrameHook.close!(e)}
|
|
117
|
+
right={
|
|
118
|
+
<div
|
|
119
|
+
onClick={onSave}
|
|
120
|
+
style={{
|
|
121
|
+
color: 'var(--primary-accent-color)',
|
|
122
|
+
fontWeight: 'bold',
|
|
123
|
+
fontSize: '16px',
|
|
124
|
+
padding: '0 8px',
|
|
125
|
+
cursor: 'pointer',
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
Save
|
|
129
|
+
</div>
|
|
130
|
+
}
|
|
131
|
+
>
|
|
132
|
+
<div ref={ref} css={css} class='note-edit-container flex-col h-100'>
|
|
133
|
+
<input
|
|
134
|
+
type='text'
|
|
135
|
+
class='input-base note-edit-title-input'
|
|
136
|
+
placeholder='Title...'
|
|
137
|
+
value={defaultTitle}
|
|
138
|
+
maxLength={50}
|
|
139
|
+
/>
|
|
140
|
+
<div class='note-edit-color-row' onClick={pickColor}>
|
|
141
|
+
<div class='flex-1'>Color Tag</div>
|
|
142
|
+
<div
|
|
143
|
+
class='note-color-preview color-preview-box'
|
|
144
|
+
style={{ backgroundColor: selectedColor || 'transparent' }}
|
|
145
|
+
></div>
|
|
146
|
+
<i class='ifc-icon ma-pencil-outline icon-btn-secondary' />
|
|
147
|
+
</div>
|
|
148
|
+
<textarea class='input-base note-edit-body-input' placeholder='Write your note...'>
|
|
149
|
+
{defaultContent}
|
|
150
|
+
</textarea>
|
|
151
|
+
</div>
|
|
152
|
+
</HeaderWithBackFrame>
|
|
153
|
+
);
|
|
154
|
+
};
|