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.
Files changed (42) hide show
  1. package/index.js +32 -2
  2. package/package.json +1 -1
  3. package/templates/common/AI_CONTEXT.md +245 -50
  4. package/templates/common/dev/cp-folder.js +2 -6
  5. package/templates/cv-starter/web/src/index.html +1 -1
  6. package/templates/doc-starter/web/src/index.html +1 -1
  7. package/templates/hello-world/web/src/index.html +1 -1
  8. package/templates/hello-world/web/src/index.tsx +54 -15
  9. package/templates/hello-world/web/src/styles/global.css +63 -0
  10. package/templates/responsive-starter/api/package.json +6 -0
  11. package/templates/responsive-starter/api/resources/config_default.json +6 -0
  12. package/templates/responsive-starter/api/resources/install.sqlite.sql +4 -0
  13. package/templates/responsive-starter/api/src/index.ts +4 -0
  14. package/templates/responsive-starter/api/src/service/root-api.ts +18 -0
  15. package/templates/responsive-starter/lupine.json +23 -0
  16. package/templates/responsive-starter/web/assets/favicon.ico +0 -0
  17. package/templates/responsive-starter/web/package.json +6 -0
  18. package/templates/responsive-starter/web/src/app-icons.ts +72 -0
  19. package/templates/responsive-starter/web/src/components/input-history-component.tsx +93 -0
  20. package/templates/responsive-starter/web/src/components/mine-about-page.tsx +86 -0
  21. package/templates/responsive-starter/web/src/components/mine-premium-page.tsx +80 -0
  22. package/templates/responsive-starter/web/src/components/mine-settings-page.tsx +219 -0
  23. package/templates/responsive-starter/web/src/components/mobile-top-search-menu.tsx +119 -0
  24. package/templates/responsive-starter/web/src/components/note-detail.tsx +116 -0
  25. package/templates/responsive-starter/web/src/components/note-edit.tsx +154 -0
  26. package/templates/responsive-starter/web/src/components/note-search-page.tsx +193 -0
  27. package/templates/responsive-starter/web/src/components/search-input.tsx +95 -0
  28. package/templates/responsive-starter/web/src/components/side-menu-content.tsx +178 -0
  29. package/templates/responsive-starter/web/src/frames/app-responsive-frame.tsx +46 -0
  30. package/templates/responsive-starter/web/src/index.html +16 -0
  31. package/templates/responsive-starter/web/src/index.tsx +42 -0
  32. package/templates/responsive-starter/web/src/pages/about-page.tsx +43 -0
  33. package/templates/responsive-starter/web/src/pages/finance-page.tsx +46 -0
  34. package/templates/responsive-starter/web/src/pages/home-page.tsx +452 -0
  35. package/templates/responsive-starter/web/src/pages/mine-page.tsx +325 -0
  36. package/templates/responsive-starter/web/src/pages/tools-page.tsx +46 -0
  37. package/templates/responsive-starter/web/src/services/local-notes-service.ts +87 -0
  38. package/templates/responsive-starter/web/src/services/mine-service.ts +45 -0
  39. package/templates/responsive-starter/web/src/styles/app.css +0 -0
  40. package/templates/responsive-starter/web/src/styles/base-css.ts +65 -0
  41. package/templates/responsive-starter/web/src/styles/global.css +2143 -0
  42. package/templates/responsive-starter/web/src/styles/theme.ts +16 -0
@@ -0,0 +1,452 @@
1
+ import {
2
+ CssProps,
3
+ HtmlVar,
4
+ MobileHeaderCenter,
5
+ MobileHeaderTitleIcon,
6
+ PageProps,
7
+ RefProps,
8
+ SliderFrame,
9
+ SliderFrameHookProps,
10
+ MobileHeaderEmptyIcon,
11
+ MobileTopSysIcon,
12
+ ActionSheetSelect,
13
+ createDragUtil,
14
+ DomUtils,
15
+ } from 'lupine.components';
16
+ import { NoteSearchComponent } from '../components/note-search-page';
17
+ import { LocalNoteProps, LocalNotesService } from '../services/local-notes-service';
18
+ import { NoteEditComponent } from '../components/note-edit';
19
+ import { NoteDetailComponent } from '../components/note-detail';
20
+
21
+ const extractText = (html: string) => {
22
+ const tmp = document.createElement('DIV');
23
+ tmp.innerHTML = html;
24
+ return tmp.textContent || tmp.innerText || '';
25
+ };
26
+
27
+ export const HomePage = async (props: PageProps) => {
28
+ let currentSearchQuery = '';
29
+ const dom = new HtmlVar('');
30
+ const sliderFrameHook: SliderFrameHookProps = {};
31
+
32
+ let draggedAmount = 0;
33
+ let menuClosedJustNow = false;
34
+
35
+ const onOpenSearch = () => {
36
+ resetSwipeMenus();
37
+ sliderFrameHook.load!(<NoteSearchComponent sliderFrameHook={sliderFrameHook} />);
38
+ };
39
+
40
+ const onAddNote = () => {
41
+ sliderFrameHook.load!(<NoteEditComponent sliderFrameHook={sliderFrameHook} onSaved={refreshList} />);
42
+ resetSwipeMenus();
43
+ };
44
+
45
+ const onEditNote = (note: LocalNoteProps) => {
46
+ sliderFrameHook.load!(<NoteEditComponent note={note} sliderFrameHook={sliderFrameHook} onSaved={refreshList} />);
47
+ };
48
+
49
+ const onViewNote = (note: LocalNoteProps) => {
50
+ resetSwipeMenus();
51
+ sliderFrameHook.load!(<NoteDetailComponent note={note} sliderFrameHook={sliderFrameHook} onSaved={refreshList} />);
52
+ };
53
+
54
+ const onDeleteNote = async (id: number, e: Event) => {
55
+ e.stopPropagation(); // prevent edit trigger
56
+ await ActionSheetSelect.show({
57
+ title: 'Are you sure you want to delete this note?',
58
+ options: ['Remove'],
59
+ cancelButtonText: 'Cancel',
60
+ handleClicked: async (index: number, close: () => void) => {
61
+ close();
62
+ if (index === 0) {
63
+ LocalNotesService.deleteNote(id);
64
+ refreshList();
65
+ }
66
+ },
67
+ });
68
+ };
69
+
70
+ const dragUtil = createDragUtil();
71
+ dragUtil.setOnMoveCallback((clientX, clientY, movedX) => {
72
+ const dragDom = dragUtil.getDraggingDom();
73
+ if (!dragDom) return;
74
+
75
+ if (dragDom.classList.contains('note-card')) {
76
+ let translateX = movedX;
77
+ if (translateX > 0) translateX = 0;
78
+ if (translateX < -100) translateX = -100;
79
+ dragDom.style.transform = `translateX(${translateX}px)`;
80
+ draggedAmount = Math.abs(movedX);
81
+
82
+ const actionLayer = dragDom.previousElementSibling as HTMLDivElement;
83
+ if (actionLayer && actionLayer.classList.contains('note-card-actions-layer')) {
84
+ actionLayer.style.opacity = translateX < -5 ? '1' : '0';
85
+ }
86
+ } else if (dragDom.classList.contains('note-card-drag-handle')) {
87
+ resetSwipeMenus();
88
+ const cardWrapper = dragDom.closest('.note-card-wrapper') as HTMLDivElement;
89
+ if (!cardWrapper) return;
90
+
91
+ // visual feedback for dragging
92
+ cardWrapper.style.opacity = '0.9';
93
+ cardWrapper.style.transform = 'scale(1.02)';
94
+ cardWrapper.style.boxShadow = '0 12px 24px rgba(0,0,0,0.15)';
95
+ cardWrapper.style.outline = '2px solid var(--primary-accent-color)';
96
+ cardWrapper.style.zIndex = '100';
97
+
98
+ const container = DomUtils.bySelector('.note-list-container') as HTMLDivElement;
99
+ if (!container) return;
100
+
101
+ const cards = container.querySelectorAll('.note-card-wrapper') as NodeListOf<HTMLDivElement>;
102
+ if (cards.length <= 1) return;
103
+ const rect = container.getBoundingClientRect();
104
+ const relativeY = clientY - rect.top + container.scrollTop;
105
+
106
+ let index = -1;
107
+ for (let i = 0; i < cards.length; i++) {
108
+ const cardTop = cards[i].offsetTop;
109
+ const cardBottom = cardTop + cards[i].offsetHeight;
110
+ if (relativeY >= cardTop && relativeY <= cardBottom) {
111
+ index = i;
112
+ break;
113
+ }
114
+ }
115
+
116
+ if (index >= 0) {
117
+ const targetCard = cards[index];
118
+ if (cardWrapper !== targetCard) {
119
+ let index2 = -1;
120
+ for (let i = 0; i < cards.length; i++) {
121
+ if (cards[i] === cardWrapper) {
122
+ index2 = i;
123
+ break;
124
+ }
125
+ }
126
+ if (index2 >= 0) {
127
+ if (index2 < index) {
128
+ targetCard.parentNode?.insertBefore(cardWrapper, targetCard.nextSibling);
129
+ } else {
130
+ targetCard.parentNode?.insertBefore(cardWrapper, targetCard);
131
+ }
132
+ }
133
+ }
134
+ }
135
+ // Set grabbed amount explicitly to avoid triggering any click events later
136
+ draggedAmount = 100;
137
+ }
138
+ });
139
+
140
+ dragUtil.setOnMoveEndCallback(() => {
141
+ const dragDom = dragUtil.getDraggingDom();
142
+ if (!dragDom) return;
143
+
144
+ if (dragDom.classList.contains('note-card')) {
145
+ const currentTransform = dragDom.style.transform;
146
+ const match = currentTransform.match(/translateX\(([-\d\.]+)px\)/);
147
+ if (match) {
148
+ const x = parseFloat(match[1]);
149
+ if (x < -50) {
150
+ dragDom.style.transform = `translateX(-100px)`;
151
+ } else {
152
+ dragDom.style.transform = `translateX(0px)`;
153
+ const actionLayer = dragDom.previousElementSibling as HTMLDivElement;
154
+ if (actionLayer && actionLayer.classList.contains('note-card-actions-layer')) {
155
+ actionLayer.style.opacity = '0';
156
+ }
157
+ }
158
+ } else {
159
+ dragDom.style.transform = `translateX(0px)`;
160
+ const actionLayer = dragDom.previousElementSibling as HTMLDivElement;
161
+ if (actionLayer && actionLayer.classList.contains('note-card-actions-layer')) {
162
+ actionLayer.style.opacity = '0';
163
+ }
164
+ }
165
+ } else if (dragDom.classList.contains('note-card-drag-handle')) {
166
+ const cardWrapper = dragDom.closest('.note-card-wrapper') as HTMLDivElement;
167
+ if (cardWrapper) {
168
+ cardWrapper.style.opacity = '1';
169
+ cardWrapper.style.transform = 'scale(1)';
170
+ cardWrapper.style.boxShadow = '0 4px 12px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.05)';
171
+ cardWrapper.style.outline = 'none';
172
+ cardWrapper.style.zIndex = '';
173
+ }
174
+
175
+ const container = DomUtils.bySelector('.note-list-container') as HTMLDivElement;
176
+ if (!container) return;
177
+ const cards = container.querySelectorAll('.note-card') as NodeListOf<HTMLDivElement>;
178
+ const newOrderIds: number[] = [];
179
+ cards.forEach((c) => newOrderIds.push(parseInt(c.getAttribute('data-id') || '0')));
180
+ if (newOrderIds.length > 0) {
181
+ LocalNotesService.updateNoteOrders(newOrderIds);
182
+ }
183
+ }
184
+ });
185
+
186
+ const resetSwipeMenus = (excludeId?: number): boolean => {
187
+ let closedAny = false;
188
+ const cards = document.querySelectorAll('.note-list-container .note-card') as NodeListOf<HTMLDivElement>;
189
+ cards.forEach((c) => {
190
+ if (excludeId !== undefined) {
191
+ const id = parseInt(c.getAttribute('data-id') || '-1');
192
+ if (id === excludeId) return;
193
+ }
194
+ const transform = c.style.transform;
195
+ const match = transform.match(/translateX\(([-\d\.]+)px\)/);
196
+ if (match && parseFloat(match[1]) < -5) {
197
+ c.style.transform = 'translateX(0px)';
198
+ const actionLayer = c.previousElementSibling as HTMLDivElement;
199
+ if (actionLayer && actionLayer.classList.contains('note-card-actions-layer')) {
200
+ actionLayer.style.opacity = '0';
201
+ }
202
+ closedAny = true;
203
+ }
204
+ });
205
+
206
+ return closedAny;
207
+ };
208
+
209
+ const handleBgTouch = (e: Event) => {
210
+ let target = e.target as Element;
211
+ if (target && target.closest && target.closest('.action-btn')) return;
212
+ if (target && target.closest && target.closest('.note-card')) return;
213
+ resetSwipeMenus();
214
+ };
215
+
216
+ const refreshList = () => {
217
+ const allNotes = LocalNotesService.getAllNotes();
218
+ const filteredNotes = currentSearchQuery
219
+ ? allNotes.filter(
220
+ (n) =>
221
+ n.title.toLowerCase().includes(currentSearchQuery.toLowerCase()) ||
222
+ n.content.toLowerCase().includes(currentSearchQuery.toLowerCase())
223
+ )
224
+ : allNotes;
225
+
226
+ if (filteredNotes.length === 0) {
227
+ dom.value = <div class='note-empty-state'>No search results</div>;
228
+ return;
229
+ }
230
+
231
+ dom.value = (
232
+ <div class='note-list-container' style={{ position: 'relative' }}>
233
+ {filteredNotes.map((note) => (
234
+ <div class='note-card-wrapper'>
235
+ <div class='note-card-actions-layer'>
236
+ <div
237
+ class='action-btn edit-btn'
238
+ onClick={(e) => {
239
+ e.stopPropagation();
240
+ onEditNote(note);
241
+ }}
242
+ >
243
+ <i class='ifc-icon ma-pencil-outline' />
244
+ </div>
245
+ <div
246
+ class='action-btn delete-btn'
247
+ onClick={(e) => {
248
+ onDeleteNote(note.id, e);
249
+ }}
250
+ >
251
+ <i class='ifc-icon ma-delete-off-outline' />
252
+ </div>
253
+ </div>
254
+ <div
255
+ class='note-card row-box'
256
+ style={{ borderLeft: note.color ? `6px solid ${note.color}` : '' }}
257
+ data-id={note.id}
258
+ onMouseDown={(e) => {
259
+ resetSwipeMenus(note.id);
260
+ draggedAmount = 0;
261
+ dragUtil.onMouseDown(e);
262
+ }}
263
+ onTouchStart={(e) => {
264
+ resetSwipeMenus(note.id);
265
+ draggedAmount = 0;
266
+ dragUtil.onTouchStart(e);
267
+ }}
268
+ onClick={(e) => {
269
+ if (draggedAmount > 5) return;
270
+ const cardDom = e.currentTarget as HTMLElement;
271
+ const transform = cardDom.style.transform;
272
+ const match = transform.match(/translateX\(([-\d\.]+)px\)/);
273
+ if (match && parseFloat(match[1]) < -5) {
274
+ // user clicked the opened card itself, just close it
275
+ cardDom.style.transform = 'translateX(0px)';
276
+ const actionLayer = cardDom.previousElementSibling as HTMLDivElement;
277
+ if (actionLayer && actionLayer.classList.contains('note-card-actions-layer')) {
278
+ actionLayer.style.opacity = '0';
279
+ }
280
+ return;
281
+ }
282
+ const closedOther = resetSwipeMenus();
283
+ if (closedOther) return; // if it was open on another card, swallow the click
284
+ onViewNote(note);
285
+ }}
286
+ >
287
+ <div class='note-card-content flex-1' style={{ minWidth: 0 }}>
288
+ <div class='note-card-title'>{note.title}</div>
289
+ <div class='note-card-preview ellipsis'>{extractText(note.content)}</div>
290
+ <div class='note-card-date'>{new Date(note.updatedAt).toLocaleString()}</div>
291
+ </div>
292
+ <div
293
+ class='note-card-drag-handle'
294
+ onMouseDown={(e) => {
295
+ e.stopPropagation();
296
+ dragUtil.onMouseDown(e);
297
+ }}
298
+ onTouchStart={(e) => {
299
+ e.stopPropagation();
300
+ dragUtil.onTouchStart(e);
301
+ }}
302
+ >
303
+ <i class='ifc-icon bs-list' />
304
+ </div>
305
+ </div>
306
+ </div>
307
+ ))}
308
+ </div>
309
+ );
310
+ };
311
+
312
+ const ref: RefProps = {
313
+ onLoad: async () => {
314
+ refreshList();
315
+ },
316
+ };
317
+
318
+ const css: CssProps = {
319
+ display: 'flex',
320
+ flexDirection: 'column',
321
+ height: '100%',
322
+ position: 'relative',
323
+
324
+ '.note-home-scroll': {
325
+ flex: 1,
326
+ overflowY: 'auto',
327
+ padding: '16px',
328
+ },
329
+ '.note-empty-state': {
330
+ display: 'flex',
331
+ justifyContent: 'center',
332
+ alignItems: 'center',
333
+ height: '100%',
334
+ color: 'var(--secondary-color)',
335
+ fontSize: '16px',
336
+ },
337
+ '.note-card': {
338
+ backgroundColor: 'var(--primary-bg-color)',
339
+ borderRadius: '12px',
340
+ padding: '16px',
341
+ cursor: 'pointer',
342
+ alignItems: 'center',
343
+ transition: 'transform 0.2s ease, box-shadow 0.2s ease',
344
+ position: 'relative',
345
+ zIndex: 2,
346
+ },
347
+ '.note-card-actions-layer': {
348
+ position: 'absolute',
349
+ top: 0,
350
+ left: 0,
351
+ right: 0,
352
+ bottom: 0,
353
+ display: 'flex',
354
+ justifyContent: 'flex-end',
355
+ alignItems: 'center',
356
+ zIndex: 1,
357
+ backgroundColor: 'transparent', // explicitly prevent background bleeding
358
+ borderRadius: '12px',
359
+ overflow: 'hidden',
360
+ opacity: 0,
361
+ transition: 'opacity 0.2s ease',
362
+ },
363
+ '.action-btn': {
364
+ height: '100%',
365
+ width: '50px',
366
+ display: 'flex',
367
+ alignItems: 'center',
368
+ justifyContent: 'center',
369
+ color: 'white',
370
+ cursor: 'pointer',
371
+ },
372
+ '.action-btn.edit-btn': { backgroundColor: 'var(--primary-accent-color)' },
373
+ '.action-btn.delete-btn': {
374
+ backgroundColor: '#ff4d4f',
375
+ borderTopRightRadius: '12px',
376
+ borderBottomRightRadius: '12px',
377
+ },
378
+ '.action-btn i': { color: 'white' },
379
+ '.note-card-drag-handle': {
380
+ padding: '8px',
381
+ cursor: 'grab',
382
+ color: 'var(--secondary-color)',
383
+ opacity: 0.5,
384
+ zIndex: 10,
385
+ },
386
+ '.note-card-drag-handle:active': { cursor: 'grabbing' },
387
+ '.note-card-title': {
388
+ fontSize: '16px',
389
+ fontWeight: 'bold',
390
+ color: 'var(--primary-color)',
391
+ marginBottom: '4px',
392
+ whiteSpace: 'nowrap',
393
+ overflow: 'hidden',
394
+ textOverflow: 'ellipsis',
395
+ },
396
+ '.note-card-preview': {
397
+ fontSize: '14px',
398
+ color: 'var(--secondary-color)',
399
+ marginBottom: '6px',
400
+ display: '-webkit-box',
401
+ WebkitLineClamp: 2,
402
+ WebkitBoxOrient: 'vertical',
403
+ overflow: 'hidden',
404
+ },
405
+ '.note-card-date': {
406
+ fontSize: '12px',
407
+ color: 'var(--secondary-color)',
408
+ opacity: 0.8,
409
+ },
410
+ '.fab-button': {
411
+ backgroundColor: 'var(--primary-accent-color, #1890ff)',
412
+ },
413
+ };
414
+
415
+ return (
416
+ <div
417
+ css={css}
418
+ ref={ref}
419
+ onMouseDown={handleBgTouch}
420
+ onTouchStart={handleBgTouch}
421
+ onMouseMove={dragUtil.onMouseMove}
422
+ onMouseUp={dragUtil.onMouseUp}
423
+ onTouchMove={dragUtil.onTouchMove}
424
+ onTouchEnd={dragUtil.onTouchEnd}
425
+ >
426
+ <MobileHeaderCenter>
427
+ <MobileHeaderTitleIcon
428
+ title='Notes'
429
+ left={<MobileHeaderEmptyIcon />}
430
+ right={
431
+ <div class='flex-center-gap-12'>
432
+ <i class='ifc-icon bs-search icon-20-pointer' onClick={onOpenSearch}></i>
433
+ <MobileTopSysIcon />
434
+ </div>
435
+ }
436
+ />
437
+ </MobileHeaderCenter>
438
+ <div class='note-home-scroll no-scrollbar-container'>
439
+ <SliderFrame
440
+ hook={sliderFrameHook}
441
+ afterClose={async () => {
442
+ await resetSwipeMenus();
443
+ }}
444
+ />
445
+ {dom.node}
446
+ </div>
447
+ <div class='fab-button' onClick={onAddNote}>
448
+ <span class='fab-icon'>+</span>
449
+ </div>
450
+ </div>
451
+ );
452
+ };