@vue-lynx-example/todomvc 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # TodoMVC
2
2
 
3
- Classic [TodoMVC](https://todomvc.com) built with Vue 3 × Lynx using CSS Selector styling.
3
+ Classic [TodoMVC](https://todomvc.com) built with Vue 3 × Lynx using CSS Selector styling. This version features iteratively improved UI, layout, and scrolling behaviors matching the modern reference clone.
4
+
5
+ *(For the original unpolished version, see `examples/todomvc-day1`)*
4
6
 
5
7
  ## Features Exercised
6
8
 
package/lynx.config.ts CHANGED
@@ -1,7 +1,23 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
1
3
  import { defineConfig } from '@lynx-js/rspeedy';
2
4
  import { pluginVueLynx } from 'vue-lynx/plugin';
3
5
 
6
+ const exampleName = path.basename(path.dirname(fileURLToPath(import.meta.url)));
7
+
4
8
  export default defineConfig({
9
+ environments: {
10
+ web: {},
11
+ lynx: {},
12
+ },
13
+ output: {
14
+ assetPrefix: `https://vue.lynxjs.org/examples/${exampleName}/dist/`,
15
+ },
16
+ source: {
17
+ entry: {
18
+ main: './src/index.ts',
19
+ },
20
+ },
5
21
  plugins: [
6
22
  pluginVueLynx({
7
23
  optionsApi: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vue-lynx-example/todomvc",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "private": false,
5
5
  "description": "TodoMVC built with Vue 3 × Lynx",
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  "directory": "examples/todomvc"
17
17
  },
18
18
  "dependencies": {
19
- "vue-lynx": "0.1.0-pre-alpha.0"
19
+ "vue-lynx": "0.2.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@lynx-js/rspeedy": "^0.13.5",
package/src/TodoApp.vue CHANGED
@@ -8,6 +8,9 @@ import TodoFooter from './TodoFooter.vue'
8
8
  // ── State ──────────────────────────────────────────────────
9
9
  const todos = ref([])
10
10
  const filter = ref('all')
11
+ const newTodo = ref('')
12
+ const editedTodoId = ref(null)
13
+ const editText = ref('')
11
14
 
12
15
  // ── Derived ────────────────────────────────────────────────
13
16
  const activeTodos = computed(() => todos.value.filter(t => !t.completed))
@@ -17,6 +20,15 @@ const filteredTodos = computed(() => {
17
20
  if (filter.value === 'completed') return completedTodos.value
18
21
  return todos.value
19
22
  })
23
+
24
+ const TODO_ROW_HEIGHT = 58;
25
+ const VIEWPORT_RESERVED_HEIGHT = 320;
26
+
27
+ const mainScrollHeight = computed(
28
+ () => `${filteredTodos.value.length * TODO_ROW_HEIGHT}px`,
29
+ )
30
+ const mainScrollMaxHeight = `calc(100vh - ${VIEWPORT_RESERVED_HEIGHT}px)`
31
+
20
32
  const allCompleted = computed(() =>
21
33
  todos.value.length > 0 && activeTodos.value.length === 0,
22
34
  )
@@ -28,9 +40,14 @@ function uuid() {
28
40
  }
29
41
 
30
42
  // ── Actions ────────────────────────────────────────────────
43
+ function updateNewTodo(value) {
44
+ newTodo.value = value
45
+ }
46
+
31
47
  function addTodo(title) {
32
48
  if (!title.trim()) return
33
49
  todos.value.push({ id: uuid(), title: title.trim(), completed: false })
50
+ newTodo.value = ''
34
51
  }
35
52
 
36
53
  function toggleTodo(todo) {
@@ -39,14 +56,36 @@ function toggleTodo(todo) {
39
56
 
40
57
  function deleteTodo(todo) {
41
58
  todos.value = todos.value.filter(t => t.id !== todo.id)
59
+ if (editedTodoId.value === todo.id) {
60
+ editedTodoId.value = null
61
+ editText.value = ''
62
+ }
63
+ }
64
+
65
+ function startEdit(todo) {
66
+ editedTodoId.value = todo.id
67
+ editText.value = todo.title
68
+ }
69
+
70
+ function updateEdit(value) {
71
+ editText.value = value
42
72
  }
43
73
 
44
- function editTodo(todo, newTitle) {
45
- if (!newTitle.trim()) {
74
+ function doneEdit(todo) {
75
+ if (editedTodoId.value !== todo.id) {
76
+ return
77
+ }
78
+
79
+ const title = editText.value.trim()
80
+ editedTodoId.value = null
81
+ editText.value = ''
82
+
83
+ if (!title) {
46
84
  deleteTodo(todo)
47
85
  return
48
86
  }
49
- todo.title = newTitle.trim()
87
+
88
+ todo.title = title
50
89
  }
51
90
 
52
91
  function toggleAll() {
@@ -64,47 +103,56 @@ function setFilter(f) {
64
103
  </script>
65
104
 
66
105
  <template>
67
- <view class="todoapp">
68
- <TodoHeader @add-todo="addTodo" />
69
-
70
- <view class="main" v-if="todos.length > 0">
71
- <!-- Toggle all -->
72
- <view class="toggle-all-container">
73
- <view
74
- class="toggle-all-btn"
75
- :class="{ 'all-completed': allCompleted }"
76
- @tap="toggleAll"
77
- >
78
- <text class="toggle-all-icon">✓</text>
79
- </view>
80
- <text class="toggle-all-label">Mark all as complete</text>
81
- </view>
106
+ <view class="page">
107
+ <view class="todoapp-shell">
108
+ <view class="todoapp">
109
+ <TodoHeader
110
+ :all-completed="allCompleted"
111
+ :has-todos="todos.length > 0"
112
+ :new-todo="newTodo"
113
+ @update-new-todo="updateNewTodo"
114
+ @add-todo="addTodo"
115
+ @toggle-all="toggleAll"
116
+ />
82
117
 
83
- <!-- Todo list -->
84
- <view class="todo-list">
85
- <TodoItem
86
- v-for="todo in filteredTodos"
87
- :key="todo.id"
88
- :todo="todo"
89
- @toggle="toggleTodo"
90
- @delete="deleteTodo"
91
- @edit="editTodo"
118
+ <scroll-view
119
+ v-if="todos.length > 0"
120
+ scroll-orientation="vertical"
121
+ :style="{ height: mainScrollHeight, maxHeight: mainScrollMaxHeight }"
122
+ >
123
+ <view class="main">
124
+ <!-- Todo list -->
125
+ <view class="todo-list">
126
+ <TodoItem
127
+ v-for="todo in filteredTodos"
128
+ :key="todo.id"
129
+ :todo="todo"
130
+ :editing="editedTodoId === todo.id"
131
+ :edit-text="editedTodoId === todo.id ? editText : todo.title"
132
+ @toggle="toggleTodo"
133
+ @delete="deleteTodo"
134
+ @start-edit="startEdit"
135
+ @update-edit="updateEdit"
136
+ @done-edit="doneEdit"
137
+ />
138
+ </view>
139
+ </view>
140
+ </scroll-view>
141
+
142
+ <TodoFooter
143
+ v-if="todos.length > 0"
144
+ :active-count="activeTodos.length"
145
+ :completed-count="completedTodos.length"
146
+ :current-filter="filter"
147
+ @set-filter="setFilter"
148
+ @clear-completed="clearCompleted"
92
149
  />
93
150
  </view>
94
151
  </view>
95
152
 
96
- <TodoFooter
97
- v-if="todos.length > 0"
98
- :active-count="activeTodos.length"
99
- :completed-count="completedTodos.length"
100
- :current-filter="filter"
101
- @set-filter="setFilter"
102
- @clear-completed="clearCompleted"
103
- />
104
-
105
153
  <!-- Info -->
106
154
  <view class="info">
107
- <text class="info-text">Tap a todo circle to toggle</text>
155
+ <text class="info-text">Tap the todo text to edit</text>
108
156
  <text class="info-text">Built with Vue 3 × Lynx</text>
109
157
  </view>
110
158
  </view>
@@ -10,9 +10,9 @@ function onClear() { emit('clear-completed') }
10
10
 
11
11
  <template>
12
12
  <view class="footer">
13
- <view :style="{ flexDirection: 'row' }">
14
- <text class="todo-count-number">{{ activeCount }}</text>
15
- <text class="todo-count"> {{ activeCount === 1 ? 'item' : 'items' }} left</text>
13
+ <view class="todo-count">
14
+ <text class="todo-count-strong">{{ activeCount }}</text>
15
+ <text class="todo-count-label"> {{ activeCount === 1 ? 'item' : 'items' }} left</text>
16
16
  </view>
17
17
 
18
18
  <view class="filters">
@@ -1,23 +1,59 @@
1
1
  <script setup>
2
- const emit = defineEmits(['add-todo'])
2
+ defineProps(['allCompleted', 'hasTodos', 'newTodo'])
3
+ const emit = defineEmits(['update-new-todo', 'add-todo', 'toggle-all'])
3
4
 
4
- function onConfirm(e) {
5
+ function onInput(e) {
5
6
  const value = e?.detail?.value ?? ''
7
+ emit('update-new-todo', value)
8
+ }
9
+
10
+ function onConfirm(e) {
11
+ let value = e?.detail?.value
12
+ if (value === undefined) {
13
+ value = ''
14
+ } else {
15
+ emit('update-new-todo', value)
16
+ }
17
+
6
18
  if (value.trim()) {
7
19
  emit('add-todo', value)
8
20
  }
9
21
  }
22
+
23
+ function onToggleAll() {
24
+ emit('toggle-all')
25
+ }
10
26
  </script>
11
27
 
12
28
  <template>
13
29
  <view class="header">
14
30
  <text class="title">todos</text>
15
- <input
16
- class="new-todo"
17
- type="text"
18
- placeholder="What needs to be done?"
19
- confirm-type="done"
20
- @confirm="onConfirm"
21
- />
31
+ <view class="new-todo-row">
32
+ <view
33
+ v-if="hasTodos"
34
+ class="header-toggle-all"
35
+ @tap="onToggleAll"
36
+ >
37
+ <text
38
+ class="header-toggle-all-icon"
39
+ :class="{ checked: allCompleted }"
40
+ >✓</text>
41
+ </view>
42
+ <view
43
+ v-else
44
+ class="header-toggle-all header-toggle-all-placeholder"
45
+ />
46
+
47
+ <input
48
+ class="new-todo with-toggle"
49
+ type="text"
50
+ :value="newTodo"
51
+ placeholder="What needs to be done?"
52
+ confirm-type="done"
53
+ autofocus
54
+ @input="onInput"
55
+ @confirm="onConfirm"
56
+ />
57
+ </view>
22
58
  </view>
23
59
  </template>
package/src/TodoItem.vue CHANGED
@@ -1,10 +1,6 @@
1
1
  <script setup>
2
- import { ref } from 'vue'
3
-
4
- const props = defineProps(['todo'])
5
- const emit = defineEmits(['toggle', 'delete', 'edit'])
6
-
7
- const editing = ref(false)
2
+ const props = defineProps(['todo', 'editing', 'editText'])
3
+ const emit = defineEmits(['toggle', 'delete', 'start-edit', 'update-edit', 'done-edit'])
8
4
 
9
5
  function onToggle() {
10
6
  emit('toggle', props.todo)
@@ -15,42 +11,53 @@ function onDelete() {
15
11
  }
16
12
 
17
13
  function startEdit() {
18
- editing.value = true
14
+ emit('start-edit', props.todo)
19
15
  }
20
16
 
21
- function onEditConfirm(e) {
17
+ function onEditInput(e) {
22
18
  const value = e?.detail?.value ?? ''
23
- editing.value = false
24
- emit('edit', props.todo, value)
19
+ emit('update-edit', value)
25
20
  }
26
21
 
27
- function cancelEdit() {
28
- editing.value = false
22
+ function onDoneEdit(e) {
23
+ if (e) {
24
+ const value = e?.detail?.value ?? ''
25
+ emit('update-edit', value)
26
+ }
27
+ emit('done-edit', props.todo)
29
28
  }
30
29
  </script>
31
30
 
32
31
  <template>
33
- <!-- Normal view -->
34
32
  <view
35
- v-if="!editing"
36
33
  class="todo-item"
37
- :class="{ completed: todo.completed }"
34
+ :class="{ completed: todo.completed, editing }"
38
35
  >
39
- <view class="todo-toggle" @tap="onToggle">
40
- <text v-if="todo.completed" class="checkmark">✓</text>
36
+ <!-- Normal view -->
37
+ <view
38
+ v-if="!editing"
39
+ class="todo-view"
40
+ >
41
+ <view class="todo-toggle" @tap="onToggle">
42
+ <text v-if="todo.completed" class="todo-toggle-icon">✓</text>
43
+ </view>
44
+ <view class="todo-label-hitbox" @tap="startEdit">
45
+ <text class="todo-label">{{ todo.title }}</text>
46
+ </view>
47
+ <text class="destroy" @tap="onDelete">✕</text>
41
48
  </view>
42
- <text class="todo-label" @longpress="startEdit">{{ todo.title }}</text>
43
- <text class="destroy" @tap="onDelete">✕</text>
44
- </view>
45
49
 
46
- <!-- Edit view -->
47
- <view v-else class="edit-container">
48
- <input
49
- class="edit-input"
50
- type="text"
51
- :value="todo.title"
52
- @confirm="onEditConfirm"
53
- @blur="cancelEdit"
54
- />
50
+ <!-- Edit view -->
51
+ <view v-else class="edit-container">
52
+ <input
53
+ class="edit-input"
54
+ type="text"
55
+ :value="editText"
56
+ autofocus
57
+ @input="onEditInput"
58
+ @confirm="onDoneEdit"
59
+ @blur="onDoneEdit"
60
+ />
61
+ </view>
55
62
  </view>
56
63
  </template>
package/src/todomvc.css CHANGED
@@ -11,12 +11,28 @@
11
11
 
12
12
  /* ─── App Container ─────────────────────────────────────── */
13
13
 
14
- .todoapp {
14
+ .page {
15
+ height: 100%;
16
+ background-color: #f5f5f5;
17
+ }
18
+
19
+ .todoapp-shell {
15
20
  display: flex;
16
- flex-direction: column;
17
- background-color: #fff;
18
- position: relative;
21
+ flex: 1;
22
+ align-items: center;
23
+ justify-content: flex-start;
24
+ width: 100%;
25
+ padding: 28px 16px 12px;
26
+ }
27
+
28
+ .todoapp {
19
29
  width: 100%;
30
+ max-width: 560px;
31
+ margin: 0 auto;
32
+ background-color: #ffffff;
33
+ box-shadow:
34
+ 0 2px 4px rgba(0, 0, 0, 0.16),
35
+ 0 24px 50px rgba(0, 0, 0, 0.08);
20
36
  }
21
37
 
22
38
  /* ─── Header ────────────────────────────────────────────── */
@@ -24,73 +40,78 @@
24
40
  .header {
25
41
  display: flex;
26
42
  flex-direction: column;
27
- align-items: center;
28
43
  padding-top: 12px;
44
+ width: 100%;
29
45
  }
30
46
 
31
47
  .header .title {
32
- font-size: 60px;
48
+ font-size: 72px;
49
+ line-height: 1;
33
50
  font-weight: 200;
34
51
  color: rgba(175, 47, 47, 0.15);
35
52
  text-align: center;
53
+ margin-bottom: 10px;
36
54
  }
37
55
 
38
- .new-todo {
39
- width: 90%;
40
- padding: 16px 16px 16px 16px;
41
- font-size: 18px;
42
- border: 1px solid #999;
43
- border-radius: 4px;
44
- margin-top: 8px;
45
- color: #4d4d4d;
46
- background-color: #fff;
47
- }
48
-
49
- /* ─── Main Section ──────────────────────────────────────── */
50
-
51
- .main {
52
- display: flex;
53
- flex-direction: column;
54
- border-top: 1px solid #e6e6e6;
55
- }
56
-
57
- /* Toggle All */
58
- .toggle-all-container {
56
+ .new-todo-row {
59
57
  display: flex;
60
58
  flex-direction: row;
61
59
  align-items: center;
62
- padding: 8px 16px;
63
- border-bottom: 1px solid #e6e6e6;
60
+ border-bottom: 1px solid #ededed;
61
+ background-color: rgba(0, 0, 0, 0.002);
62
+ box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
63
+ overflow: hidden;
64
+ width: 100%;
64
65
  }
65
66
 
66
- .toggle-all-btn {
67
- width: 36px;
68
- height: 36px;
67
+ .header-toggle-all {
69
68
  display: flex;
70
69
  align-items: center;
71
70
  justify-content: center;
72
- border-radius: 18px;
71
+ width: 58px;
72
+ min-height: 60px;
73
+ flex-shrink: 0;
73
74
  }
74
75
 
75
- .toggle-all-btn.all-completed {
76
- background-color: #737373;
76
+ .header-toggle-all-placeholder {
77
+ opacity: 0;
77
78
  }
78
79
 
79
- .toggle-all-icon {
80
- font-size: 20px;
80
+ .header-toggle-all-icon {
81
+ text-align: center;
82
+ font-size: 24px;
81
83
  color: #e6e6e6;
82
84
  }
83
85
 
84
- .toggle-all-btn.all-completed .toggle-all-icon {
85
- color: #fff;
86
+ .header-toggle-all-icon.checked {
87
+ color: #737373;
86
88
  }
87
89
 
88
- .toggle-all-label {
89
- font-size: 14px;
90
- color: #e6e6e6;
91
- margin-left: 8px;
90
+ .new-todo {
91
+ flex: 1;
92
+ width: auto;
93
+ min-height: 60px;
94
+ padding: 16px 18px;
95
+ border-width: 0;
96
+ color: #4d4d4d;
97
+ font-size: 24px;
98
+ overflow: hidden;
99
+ text-overflow: clip;
92
100
  }
93
101
 
102
+ .new-todo.with-toggle {
103
+ padding-left: 6px;
104
+ }
105
+
106
+ /* ─── Main Section ──────────────────────────────────────── */
107
+
108
+ .main {
109
+ display: flex;
110
+ flex-direction: column;
111
+ }
112
+
113
+ /* Toggle All */
114
+
94
115
  /* ─── Todo List ─────────────────────────────────────────── */
95
116
 
96
117
  .todo-list {
@@ -101,19 +122,26 @@
101
122
  /* ─── Todo Item ─────────────────────────────────────────── */
102
123
 
103
124
  .todo-item {
125
+ display: flex;
126
+ flex-direction: column;
127
+ background-color: #ffffff;
128
+ border-bottom: 1px solid #ededed;
129
+ }
130
+
131
+ .todo-view {
104
132
  display: flex;
105
133
  flex-direction: row;
106
134
  align-items: center;
107
- padding: 12px 16px;
108
- border-bottom: 1px solid #ededed;
135
+ min-height: 58px;
136
+ padding: 12px 18px;
109
137
  }
110
138
 
111
139
  /* Checkbox circle */
112
140
  .todo-toggle {
113
- width: 32px;
114
- height: 32px;
115
- border-radius: 16px;
116
- border: 2px solid #bddad5;
141
+ width: 34px;
142
+ height: 34px;
143
+ border-radius: 17px;
144
+ border: 1px solid #d9e6e4;
117
145
  display: flex;
118
146
  align-items: center;
119
147
  justify-content: center;
@@ -122,23 +150,24 @@
122
150
 
123
151
  .todo-item.completed .todo-toggle {
124
152
  border-color: #5dc2af;
125
- background-color: #5dc2af;
126
153
  }
127
154
 
128
- .checkmark {
155
+ .todo-toggle-icon {
129
156
  font-size: 18px;
130
- color: #fff;
131
- font-weight: 700;
157
+ color: #5dc2af;
132
158
  }
133
159
 
134
160
  /* Todo label */
135
- .todo-label {
161
+ .todo-label-hitbox {
136
162
  flex: 1;
137
- font-size: 18px;
138
- color: #4d4d4d;
139
- margin-left: 12px;
163
+ margin-left: 14px;
140
164
  margin-right: 12px;
141
- line-height: 1.4;
165
+ }
166
+
167
+ .todo-label {
168
+ font-size: 24px;
169
+ color: #4d4d4d;
170
+ line-height: 1.3;
142
171
  }
143
172
 
144
173
  .todo-item.completed .todo-label {
@@ -162,19 +191,28 @@
162
191
  .edit-container {
163
192
  display: flex;
164
193
  flex-direction: row;
165
- align-items: center;
166
- padding: 4px 16px;
194
+ padding: 0 0 0 52px;
167
195
  border-bottom: 1px solid #ededed;
196
+ overflow: hidden;
197
+ }
198
+
199
+ .todo-item.editing .edit-container {
200
+ border-bottom-width: 0;
168
201
  }
169
202
 
170
203
  .edit-input {
204
+ width: auto;
171
205
  flex: 1;
172
- font-size: 18px;
173
- padding: 12px;
174
- border: 1px solid #999;
175
- border-radius: 4px;
206
+ box-sizing: border-box;
207
+ min-height: 58px;
208
+ padding: 12px 16px;
209
+ border: 1px solid #999999;
210
+ background-color: #ffffff;
176
211
  color: #4d4d4d;
177
- background-color: #fff;
212
+ font-size: 24px;
213
+ box-shadow:
214
+ inset 0 -1px 5px rgba(0, 0, 0, 0.2),
215
+ 0 0 1px rgba(0, 0, 0, 0.12);
178
216
  }
179
217
 
180
218
  /* ─── Footer ────────────────────────────────────────────── */
@@ -183,20 +221,37 @@
183
221
  display: flex;
184
222
  flex-direction: row;
185
223
  align-items: center;
186
- padding: 10px 16px;
224
+ justify-content: space-between;
225
+ flex-wrap: wrap;
226
+ gap: 10px;
227
+ padding: 12px 16px;
228
+ color: #777777;
187
229
  border-top: 1px solid #e6e6e6;
230
+ box-shadow:
231
+ 0 1px 1px rgba(0, 0, 0, 0.08),
232
+ 0 8px 0 -3px #f6f6f6,
233
+ 0 9px 1px -3px rgba(0, 0, 0, 0.08),
234
+ 0 16px 0 -6px #f6f6f6,
235
+ 0 17px 2px -6px rgba(0, 0, 0, 0.08);
188
236
  }
189
237
 
190
238
  .todo-count {
191
- font-size: 13px;
192
- color: #777;
193
- flex-shrink: 0;
239
+ display: flex;
240
+ flex-direction: row;
241
+ align-items: center;
242
+ font-size: 14px;
243
+ color: #777777;
194
244
  }
195
245
 
196
- .todo-count-number {
246
+ .todo-count-strong {
247
+ font-size: 14px;
197
248
  font-weight: 700;
198
- font-size: 13px;
199
- color: #777;
249
+ color: #777777;
250
+ }
251
+
252
+ .todo-count-label {
253
+ font-size: 14px;
254
+ color: #777777;
200
255
  }
201
256
 
202
257
  /* Filter buttons */
@@ -205,15 +260,17 @@
205
260
  flex-direction: row;
206
261
  flex: 1;
207
262
  justify-content: center;
208
- gap: 4px;
263
+ flex-wrap: wrap;
264
+ gap: 8px;
265
+ min-width: 180px;
209
266
  }
210
267
 
211
268
  .filter-btn {
212
- font-size: 13px;
213
- color: #777;
214
269
  padding: 3px 8px;
215
270
  border: 1px solid transparent;
216
271
  border-radius: 3px;
272
+ color: #777777;
273
+ font-size: 14px;
217
274
  }
218
275
 
219
276
  .filter-btn.selected {
@@ -222,9 +279,8 @@
222
279
 
223
280
  /* Clear completed */
224
281
  .clear-completed {
225
- font-size: 13px;
226
- color: #777;
227
- flex-shrink: 0;
282
+ font-size: 14px;
283
+ color: #777777;
228
284
  }
229
285
 
230
286
  /* ─── Info Footer ───────────────────────────────────────── */
Binary file