@vue-lynx-example/todomvc 0.1.1

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 ADDED
@@ -0,0 +1,14 @@
1
+ # TodoMVC
2
+
3
+ Classic [TodoMVC](https://todomvc.com) built with Vue 3 × Lynx using CSS Selector styling.
4
+
5
+ ## Features Exercised
6
+
7
+ - `<script setup>` with `defineProps` / `defineEmits`
8
+ - `ref`, `computed` for state management
9
+ - `v-for` list rendering with `:key`
10
+ - `v-if` / `v-else` conditional rendering
11
+ - Dynamic `:class` binding
12
+ - `@tap`, `@longpress`, `@confirm`, `@blur` events
13
+ - CSS Selectors (`enableCSSSelector: true`)
14
+ - Multi-component composition (TodoApp, TodoHeader, TodoItem, TodoFooter)
Binary file
package/lynx.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from '@lynx-js/rspeedy';
2
+ import { pluginVueLynx } from 'vue-lynx/plugin';
3
+
4
+ export default defineConfig({
5
+ plugins: [
6
+ pluginVueLynx({
7
+ optionsApi: false,
8
+ enableCSSSelector: true,
9
+ }),
10
+ ],
11
+ });
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@vue-lynx-example/todomvc",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "description": "TodoMVC built with Vue 3 × Lynx",
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "src",
10
+ "lynx.config.ts",
11
+ "tsconfig.json"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/Huxpro/vue-lynx",
16
+ "directory": "examples/todomvc"
17
+ },
18
+ "scripts": {
19
+ "build": "rspeedy build",
20
+ "dev": "rspeedy dev"
21
+ },
22
+ "dependencies": {
23
+ "vue-lynx": "workspace:*"
24
+ },
25
+ "devDependencies": {
26
+ "@lynx-js/rspeedy": "^0.13.5",
27
+ "@rsbuild/plugin-vue": "^1.2.6",
28
+ "typescript": "^5.0.0"
29
+ },
30
+ "engines": {
31
+ "node": ">=18"
32
+ }
33
+ }
@@ -0,0 +1,111 @@
1
+ <script setup>
2
+ import { ref, computed } from 'vue'
3
+
4
+ import TodoHeader from './TodoHeader.vue'
5
+ import TodoItem from './TodoItem.vue'
6
+ import TodoFooter from './TodoFooter.vue'
7
+
8
+ // ── State ──────────────────────────────────────────────────
9
+ const todos = ref([])
10
+ const filter = ref('all')
11
+
12
+ // ── Derived ────────────────────────────────────────────────
13
+ const activeTodos = computed(() => todos.value.filter(t => !t.completed))
14
+ const completedTodos = computed(() => todos.value.filter(t => t.completed))
15
+ const filteredTodos = computed(() => {
16
+ if (filter.value === 'active') return activeTodos.value
17
+ if (filter.value === 'completed') return completedTodos.value
18
+ return todos.value
19
+ })
20
+ const allCompleted = computed(() =>
21
+ todos.value.length > 0 && activeTodos.value.length === 0,
22
+ )
23
+
24
+ // ── Helpers ────────────────────────────────────────────────
25
+ let nextId = 0
26
+ function uuid() {
27
+ return `todo-${++nextId}`
28
+ }
29
+
30
+ // ── Actions ────────────────────────────────────────────────
31
+ function addTodo(title) {
32
+ if (!title.trim()) return
33
+ todos.value.push({ id: uuid(), title: title.trim(), completed: false })
34
+ }
35
+
36
+ function toggleTodo(todo) {
37
+ todo.completed = !todo.completed
38
+ }
39
+
40
+ function deleteTodo(todo) {
41
+ todos.value = todos.value.filter(t => t.id !== todo.id)
42
+ }
43
+
44
+ function editTodo(todo, newTitle) {
45
+ if (!newTitle.trim()) {
46
+ deleteTodo(todo)
47
+ return
48
+ }
49
+ todo.title = newTitle.trim()
50
+ }
51
+
52
+ function toggleAll() {
53
+ const newVal = !allCompleted.value
54
+ todos.value.forEach(t => { t.completed = newVal })
55
+ }
56
+
57
+ function clearCompleted() {
58
+ todos.value = todos.value.filter(t => !t.completed)
59
+ }
60
+
61
+ function setFilter(f) {
62
+ filter.value = f
63
+ }
64
+ </script>
65
+
66
+ <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>
82
+
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"
92
+ />
93
+ </view>
94
+ </view>
95
+
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
+ <!-- Info -->
106
+ <view class="info">
107
+ <text class="info-text">Tap a todo circle to toggle</text>
108
+ <text class="info-text">Built with Vue 3 × Lynx</text>
109
+ </view>
110
+ </view>
111
+ </template>
@@ -0,0 +1,42 @@
1
+ <script setup>
2
+ defineProps(['activeCount', 'completedCount', 'currentFilter'])
3
+ const emit = defineEmits(['set-filter', 'clear-completed'])
4
+
5
+ function onAll() { emit('set-filter', 'all') }
6
+ function onActive() { emit('set-filter', 'active') }
7
+ function onCompleted() { emit('set-filter', 'completed') }
8
+ function onClear() { emit('clear-completed') }
9
+ </script>
10
+
11
+ <template>
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>
16
+ </view>
17
+
18
+ <view class="filters">
19
+ <text
20
+ class="filter-btn"
21
+ :class="{ selected: currentFilter === 'all' }"
22
+ @tap="onAll"
23
+ >All</text>
24
+ <text
25
+ class="filter-btn"
26
+ :class="{ selected: currentFilter === 'active' }"
27
+ @tap="onActive"
28
+ >Active</text>
29
+ <text
30
+ class="filter-btn"
31
+ :class="{ selected: currentFilter === 'completed' }"
32
+ @tap="onCompleted"
33
+ >Completed</text>
34
+ </view>
35
+
36
+ <text
37
+ v-if="completedCount > 0"
38
+ class="clear-completed"
39
+ @tap="onClear"
40
+ >Clear completed</text>
41
+ </view>
42
+ </template>
@@ -0,0 +1,23 @@
1
+ <script setup>
2
+ const emit = defineEmits(['add-todo'])
3
+
4
+ function onConfirm(e) {
5
+ const value = e?.detail?.value ?? ''
6
+ if (value.trim()) {
7
+ emit('add-todo', value)
8
+ }
9
+ }
10
+ </script>
11
+
12
+ <template>
13
+ <view class="header">
14
+ <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
+ />
22
+ </view>
23
+ </template>
@@ -0,0 +1,56 @@
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)
8
+
9
+ function onToggle() {
10
+ emit('toggle', props.todo)
11
+ }
12
+
13
+ function onDelete() {
14
+ emit('delete', props.todo)
15
+ }
16
+
17
+ function startEdit() {
18
+ editing.value = true
19
+ }
20
+
21
+ function onEditConfirm(e) {
22
+ const value = e?.detail?.value ?? ''
23
+ editing.value = false
24
+ emit('edit', props.todo, value)
25
+ }
26
+
27
+ function cancelEdit() {
28
+ editing.value = false
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <!-- Normal view -->
34
+ <view
35
+ v-if="!editing"
36
+ class="todo-item"
37
+ :class="{ completed: todo.completed }"
38
+ >
39
+ <view class="todo-toggle" @tap="onToggle">
40
+ <text v-if="todo.completed" class="checkmark">✓</text>
41
+ </view>
42
+ <text class="todo-label" @longpress="startEdit">{{ todo.title }}</text>
43
+ <text class="destroy" @tap="onDelete">✕</text>
44
+ </view>
45
+
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
+ />
55
+ </view>
56
+ </template>
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ import './todomvc.css';
2
+ import { createApp } from 'vue-lynx';
3
+
4
+ import TodoApp from './TodoApp.vue';
5
+
6
+ const app = createApp(TodoApp);
7
+ app.mount();
@@ -0,0 +1,245 @@
1
+ /*
2
+ * TodoMVC CSS for Lynx
3
+ *
4
+ * Adapted from todomvc-app-css for Lynx constraints:
5
+ * - No pseudo-elements (::before, ::after)
6
+ * - No :hover, :focus, :checked pseudo-classes
7
+ * - All elements are <view>, <text>, <input>
8
+ * - Class selectors + descendant/child combinators only
9
+ * - Lynx defaults to border-box, linear layout
10
+ */
11
+
12
+ /* ─── App Container ─────────────────────────────────────── */
13
+
14
+ .todoapp {
15
+ display: flex;
16
+ flex-direction: column;
17
+ background-color: #fff;
18
+ position: relative;
19
+ width: 100%;
20
+ }
21
+
22
+ /* ─── Header ────────────────────────────────────────────── */
23
+
24
+ .header {
25
+ display: flex;
26
+ flex-direction: column;
27
+ align-items: center;
28
+ padding-top: 12px;
29
+ }
30
+
31
+ .header .title {
32
+ font-size: 60px;
33
+ font-weight: 200;
34
+ color: rgba(175, 47, 47, 0.15);
35
+ text-align: center;
36
+ }
37
+
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 {
59
+ display: flex;
60
+ flex-direction: row;
61
+ align-items: center;
62
+ padding: 8px 16px;
63
+ border-bottom: 1px solid #e6e6e6;
64
+ }
65
+
66
+ .toggle-all-btn {
67
+ width: 36px;
68
+ height: 36px;
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ border-radius: 18px;
73
+ }
74
+
75
+ .toggle-all-btn.all-completed {
76
+ background-color: #737373;
77
+ }
78
+
79
+ .toggle-all-icon {
80
+ font-size: 20px;
81
+ color: #e6e6e6;
82
+ }
83
+
84
+ .toggle-all-btn.all-completed .toggle-all-icon {
85
+ color: #fff;
86
+ }
87
+
88
+ .toggle-all-label {
89
+ font-size: 14px;
90
+ color: #e6e6e6;
91
+ margin-left: 8px;
92
+ }
93
+
94
+ /* ─── Todo List ─────────────────────────────────────────── */
95
+
96
+ .todo-list {
97
+ display: flex;
98
+ flex-direction: column;
99
+ }
100
+
101
+ /* ─── Todo Item ─────────────────────────────────────────── */
102
+
103
+ .todo-item {
104
+ display: flex;
105
+ flex-direction: row;
106
+ align-items: center;
107
+ padding: 12px 16px;
108
+ border-bottom: 1px solid #ededed;
109
+ }
110
+
111
+ /* Checkbox circle */
112
+ .todo-toggle {
113
+ width: 32px;
114
+ height: 32px;
115
+ border-radius: 16px;
116
+ border: 2px solid #bddad5;
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ flex-shrink: 0;
121
+ }
122
+
123
+ .todo-item.completed .todo-toggle {
124
+ border-color: #5dc2af;
125
+ background-color: #5dc2af;
126
+ }
127
+
128
+ .checkmark {
129
+ font-size: 18px;
130
+ color: #fff;
131
+ font-weight: 700;
132
+ }
133
+
134
+ /* Todo label */
135
+ .todo-label {
136
+ flex: 1;
137
+ font-size: 18px;
138
+ color: #4d4d4d;
139
+ margin-left: 12px;
140
+ margin-right: 12px;
141
+ line-height: 1.4;
142
+ }
143
+
144
+ .todo-item.completed .todo-label {
145
+ color: #d9d9d9;
146
+ text-decoration: line-through;
147
+ }
148
+
149
+ /* Destroy button */
150
+ .destroy {
151
+ width: 28px;
152
+ height: 28px;
153
+ font-size: 22px;
154
+ color: #cc9a9a;
155
+ text-align: center;
156
+ line-height: 28px;
157
+ flex-shrink: 0;
158
+ }
159
+
160
+ /* ─── Edit mode ─────────────────────────────────────────── */
161
+
162
+ .edit-container {
163
+ display: flex;
164
+ flex-direction: row;
165
+ align-items: center;
166
+ padding: 4px 16px;
167
+ border-bottom: 1px solid #ededed;
168
+ }
169
+
170
+ .edit-input {
171
+ flex: 1;
172
+ font-size: 18px;
173
+ padding: 12px;
174
+ border: 1px solid #999;
175
+ border-radius: 4px;
176
+ color: #4d4d4d;
177
+ background-color: #fff;
178
+ }
179
+
180
+ /* ─── Footer ────────────────────────────────────────────── */
181
+
182
+ .footer {
183
+ display: flex;
184
+ flex-direction: row;
185
+ align-items: center;
186
+ padding: 10px 16px;
187
+ border-top: 1px solid #e6e6e6;
188
+ }
189
+
190
+ .todo-count {
191
+ font-size: 13px;
192
+ color: #777;
193
+ flex-shrink: 0;
194
+ }
195
+
196
+ .todo-count-number {
197
+ font-weight: 700;
198
+ font-size: 13px;
199
+ color: #777;
200
+ }
201
+
202
+ /* Filter buttons */
203
+ .filters {
204
+ display: flex;
205
+ flex-direction: row;
206
+ flex: 1;
207
+ justify-content: center;
208
+ gap: 4px;
209
+ }
210
+
211
+ .filter-btn {
212
+ font-size: 13px;
213
+ color: #777;
214
+ padding: 3px 8px;
215
+ border: 1px solid transparent;
216
+ border-radius: 3px;
217
+ }
218
+
219
+ .filter-btn.selected {
220
+ border-color: rgba(175, 47, 47, 0.2);
221
+ }
222
+
223
+ /* Clear completed */
224
+ .clear-completed {
225
+ font-size: 13px;
226
+ color: #777;
227
+ flex-shrink: 0;
228
+ }
229
+
230
+ /* ─── Info Footer ───────────────────────────────────────── */
231
+
232
+ .info {
233
+ display: flex;
234
+ flex-direction: column;
235
+ align-items: center;
236
+ margin-top: 24px;
237
+ padding: 0 16px;
238
+ }
239
+
240
+ .info-text {
241
+ font-size: 11px;
242
+ color: #bfbfbf;
243
+ text-align: center;
244
+ margin-top: 4px;
245
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "noEmit": true,
6
+ "noUnusedLocals": false,
7
+ "noUnusedParameters": false,
8
+ },
9
+ "include": ["src", "lynx.config.ts"],
10
+ }