noslop 0.1.0

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/dist/tui.js ADDED
@@ -0,0 +1,372 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect, useMemo } from 'react';
3
+ import { render, Box, Text, useInput, useApp, useStdout } from 'ink';
4
+ import Link from 'ink-link';
5
+ import fs from 'fs';
6
+ import { getContentDirs, parseContent, updateStatus as updateStatusLib, moveToPosts as moveToPostsLib, moveToDrafts, addPublishedUrl, deleteDraft, } from './lib/content.js';
7
+ /** Month abbreviations for date display */
8
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
9
+ /**
10
+ * Get a date key (YYYY-MM-DD) for a given day offset from today
11
+ * @param offset - Number of days from today (0 = today, 1 = tomorrow, -1 = yesterday)
12
+ * @returns Date string in YYYY-MM-DD format
13
+ */
14
+ function getDateKey(offset) {
15
+ const date = new Date();
16
+ date.setDate(date.getDate() + offset);
17
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
18
+ }
19
+ /**
20
+ * Get a display-friendly date string for a given day offset
21
+ * @param offset - Number of days from today
22
+ * @returns Date string like "Jan 15"
23
+ */
24
+ function getDateDisplay(offset) {
25
+ const date = new Date();
26
+ date.setDate(date.getDate() + offset);
27
+ return `${MONTHS[date.getMonth()]} ${date.getDate()}`;
28
+ }
29
+ /**
30
+ * Get color for day name in calendar based on selection/today state
31
+ * @param isSelected - Whether the day is currently selected
32
+ * @param isToday - Whether the day is today
33
+ * @returns Color string for the day name
34
+ */
35
+ function getDayNameColor(isSelected, isToday) {
36
+ if (isSelected) {
37
+ return 'yellow';
38
+ }
39
+ if (isToday) {
40
+ return 'green';
41
+ }
42
+ return 'white';
43
+ }
44
+ /**
45
+ * Get color for calendar item based on selection/posted state
46
+ * @param isItemSelected - Whether the item is currently selected
47
+ * @param isPosted - Whether the item has been posted
48
+ * @returns Color string for the calendar item
49
+ */
50
+ function getCalendarItemColor(isItemSelected, isPosted) {
51
+ if (isItemSelected) {
52
+ return 'black';
53
+ }
54
+ if (isPosted) {
55
+ return 'green';
56
+ }
57
+ return 'cyan';
58
+ }
59
+ /**
60
+ * Header component displaying the app title
61
+ * @param props.width - Terminal width for centering
62
+ */
63
+ function Header({ width }) {
64
+ return (_jsx(Box, { width: width, justifyContent: "center", paddingY: 1, children: _jsx(Text, { bold: true, color: "magenta", children: "\u2726 NOSLOP CONTENT MANAGER \u2726" }) }));
65
+ }
66
+ // Tabs
67
+ function Tabs({ active, width, scheduleMode, currentDateDisplay, draftsCount, postsCount, }) {
68
+ const draftsCountStr = draftsCount === 0 ? 'NO' : String(draftsCount);
69
+ const postsCountStr = postsCount === 0 ? 'NO' : String(postsCount);
70
+ const draftsLabel = scheduleMode
71
+ ? ` ${draftsCountStr} DRAFTS on ${currentDateDisplay} `
72
+ : ` ${draftsCountStr} DRAFTS `;
73
+ const postsLabel = scheduleMode
74
+ ? ` ${postsCountStr} POSTS on ${currentDateDisplay} `
75
+ : ` ${postsCountStr} POSTS `;
76
+ return (_jsx(Box, { width: width, borderStyle: "round", borderColor: "magenta", children: _jsxs(Box, { paddingX: 2, children: [_jsx(Text, { backgroundColor: active === 'drafts' ? 'yellow' : undefined, color: active === 'drafts' ? 'black' : 'gray', children: draftsLabel }), _jsx(Text, { color: "gray", children: " \u2502 " }), _jsx(Text, { backgroundColor: active === 'posts' ? 'green' : undefined, color: active === 'posts' ? 'black' : 'gray', children: postsLabel }), _jsxs(Text, { color: "gray", children: [" Tab \u2191\u2193 s", scheduleMode ? ' ←→' : '', " q"] })] }) }));
77
+ }
78
+ // Drafts List
79
+ function DraftsList({ drafts, selectedIndex, height, width, }) {
80
+ const ready = drafts.filter(d => d.status === 'ready');
81
+ const inProgress = drafts.filter(d => d.status !== 'ready');
82
+ const titleWidth = width - 16;
83
+ const visibleCount = height - 3;
84
+ const scrollOffset = Math.max(0, Math.min(selectedIndex - Math.floor(visibleCount / 2), drafts.length - visibleCount));
85
+ const visibleDrafts = drafts.slice(scrollOffset, scrollOffset + visibleCount);
86
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", height: height, width: width, children: [_jsxs(Box, { paddingX: 1, children: [_jsxs(Text, { bold: true, color: "green", children: ["READY", ' '] }), _jsxs(Text, { color: "gray", children: ["(", ready.length, ")"] }), _jsx(Text, { color: "gray", children: " \u2502 " }), _jsxs(Text, { bold: true, color: "yellow", children: ["WIP", ' '] }), _jsxs(Text, { color: "gray", children: ["(", inProgress.length, ")"] }), drafts.length > visibleCount && (_jsxs(Text, { color: "gray", children: [' ', "[", scrollOffset + 1, "-", Math.min(scrollOffset + visibleCount, drafts.length), "]"] }))] }), drafts.length === 0 ? (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "gray", children: "(empty)" }) })) : (visibleDrafts.map((item, i) => {
87
+ const actualIndex = scrollOffset + i;
88
+ const selected = actualIndex === selectedIndex;
89
+ const hasMedia = item.media && item.media !== 'None';
90
+ const idNum = item.folder.match(/^(\d{3})/)?.[1] || '---';
91
+ const title = item.title.length > titleWidth
92
+ ? `${item.title.slice(0, titleWidth - 3)}...`
93
+ : item.title;
94
+ const isReady = item.status === 'ready';
95
+ return (_jsxs(Box, { paddingX: 1, width: width - 2, children: [_jsx(Text, { color: selected ? 'yellow' : 'gray', children: selected ? '> ' : ' ' }), _jsx(Text, { color: isReady ? 'green' : 'yellow', children: isReady ? '✓ ' : '○ ' }), _jsxs(Text, { color: selected ? 'white' : 'gray', children: [idNum, " "] }), _jsx(Text, { color: "magenta", children: hasMedia ? '+ ' : ' ' }), _jsx(Text, { color: selected ? 'white' : 'gray', wrap: "truncate", children: title })] }, item.id));
96
+ }))] }));
97
+ }
98
+ // Posts List
99
+ function PostsList({ posts, selectedIndex, height, width, }) {
100
+ const titleWidth = width - 16;
101
+ const visibleCount = height - 3;
102
+ const scrollOffset = Math.max(0, Math.min(selectedIndex - Math.floor(visibleCount / 2), posts.length - visibleCount));
103
+ const visiblePosts = posts.slice(scrollOffset, scrollOffset + visibleCount);
104
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", height: height, width: width, children: [_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { bold: true, color: "green", children: "POSTS" }), posts.length > visibleCount && (_jsxs(Text, { color: "gray", children: [' ', "(", scrollOffset + 1, "-", Math.min(scrollOffset + visibleCount, posts.length), "/", posts.length, ")"] }))] }), posts.length === 0 ? (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "gray", children: "(empty)" }) })) : (visiblePosts.map((item, i) => {
105
+ const actualIndex = scrollOffset + i;
106
+ const selected = actualIndex === selectedIndex;
107
+ const hasUrl = item.published && item.published.startsWith('http');
108
+ const hasMedia = item.media && item.media !== 'None';
109
+ const idNum = item.folder.match(/^(\d{3})/)?.[1] || '???';
110
+ const title = item.title.length > titleWidth
111
+ ? `${item.title.slice(0, titleWidth - 3)}...`
112
+ : item.title;
113
+ return (_jsxs(Box, { paddingX: 1, width: width - 2, children: [_jsx(Text, { color: selected ? 'yellow' : 'gray', children: selected ? '> ' : ' ' }), _jsxs(Text, { color: selected ? 'white' : 'gray', children: [idNum, " "] }), _jsx(Text, { color: hasUrl ? 'green' : 'gray', children: hasUrl ? '✓' : ' ' }), _jsx(Text, { color: "magenta", children: hasMedia ? '+ ' : ' ' }), _jsx(Text, { color: selected ? 'white' : 'gray', wrap: "truncate", children: title })] }, item.id));
114
+ }))] }));
115
+ }
116
+ // Preview
117
+ function Preview({ item, width, height, isPosts, }) {
118
+ if (!item) {
119
+ return (_jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", width: width, height: height, padding: 1, children: _jsx(Text, { color: "gray", children: "(select an item)" }) }));
120
+ }
121
+ const contentWidth = width - 4;
122
+ const maxLines = height - 12;
123
+ const hasMedia = item.media && item.media !== 'None';
124
+ const isReady = item.status === 'ready';
125
+ const idNum = item.folder.match(/^(\d{3})/)?.[1] || '---';
126
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", width: width, height: height, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: idNum }), _jsx(Text, { color: "gray", children: " \u2014 " }), _jsx(Text, { bold: true, color: "white", children: item.title }), hasMedia && _jsx(Text, { color: "magenta", children: " +" }), !isPosts && (_jsxs(Text, { color: isReady ? 'green' : 'yellow', children: [" [", isReady ? 'READY' : 'WIP', "]"] }))] }), _jsx(Text, { color: "gray", children: '─'.repeat(contentWidth) }), _jsx(Box, { flexDirection: "column", height: maxLines, overflow: "hidden", children: _jsx(Text, { color: "white", wrap: "wrap", children: item.post }) }), _jsx(Text, { color: "gray", children: '─'.repeat(contentWidth) }), _jsxs(Box, { flexDirection: "column", children: [hasMedia ? (_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "magenta", children: "Media: " }), _jsx(Text, { color: "white", children: item.media })] })) : (_jsx(Text, { color: "gray", children: "Media: none" })), (() => {
127
+ if (item.postedAt) {
128
+ return (_jsxs(Text, { children: [_jsx(Text, { color: "green", children: "Posted: " }), _jsx(Text, { color: "white", children: item.postedAt })] }));
129
+ }
130
+ if (item.scheduledAt) {
131
+ return (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "Scheduled: " }), _jsx(Text, { color: "white", children: item.scheduledAt })] }));
132
+ }
133
+ return _jsx(Text, { color: "gray", children: "Not scheduled" });
134
+ })(), item.published && item.published.startsWith('http') ? (_jsxs(Text, { children: [_jsx(Text, { color: "green", children: "\u2713 " }), _jsx(Link, { url: item.published, fallback: false, children: _jsx(Text, { color: "cyan", children: item.published.slice(0, contentWidth) }) })] })) : (_jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "\u25CB " }), _jsx(Text, { color: "gray", children: "not published" })] }))] }), _jsx(Text, { color: "gray", children: '─'.repeat(contentWidth) }), _jsx(Box, { children: (() => {
135
+ if (isPosts) {
136
+ if (item.published?.startsWith('http')) {
137
+ return _jsx(Text, { color: "gray", children: "Space: unpost" });
138
+ }
139
+ return _jsx(Text, { color: "gray", children: "Enter: add URL \u2502 Space: unpost" });
140
+ }
141
+ return (_jsxs(Text, { color: "gray", children: ["Enter: toggle ready \u2502 Space: post", !isReady ? ' │ Backspace: delete' : ''] }));
142
+ })() })] }));
143
+ }
144
+ // Calendar
145
+ function Calendar({ selectedId, width, posts, drafts, selectedDay, }) {
146
+ const today = new Date();
147
+ const dayWidth = Math.floor((width - 2) / 7);
148
+ const days = [];
149
+ const selectedDate = new Date(today);
150
+ selectedDate.setDate(selectedDate.getDate() + selectedDay);
151
+ const dayOfWeek = selectedDate.getDay();
152
+ const daysFromMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
153
+ const mondayOffset = selectedDay - daysFromMonday;
154
+ const toDateKey = (dateStr) => {
155
+ if (!dateStr) {
156
+ return null;
157
+ }
158
+ const [datePart] = dateStr.split(' ');
159
+ return datePart;
160
+ };
161
+ const postedItems = posts
162
+ .filter(p => p.postedAt)
163
+ .map(p => ({
164
+ dateKey: toDateKey(p.postedAt),
165
+ id: p.id,
166
+ title: p.title,
167
+ type: 'posted',
168
+ }));
169
+ const scheduledItems = drafts
170
+ .filter(d => d.scheduledAt)
171
+ .map(d => ({
172
+ dateKey: toDateKey(d.scheduledAt),
173
+ id: d.id,
174
+ title: d.title,
175
+ type: 'scheduled',
176
+ }));
177
+ const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
178
+ for (let i = 0; i < 7; i++) {
179
+ const dayOffset = mondayOffset + i;
180
+ const date = new Date(today);
181
+ date.setDate(date.getDate() + dayOffset);
182
+ const dateKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
183
+ const monthDay = `${MONTHS[date.getMonth()]} ${date.getDate()}`;
184
+ const dayName = dayOffset === 0 ? 'TODAY' : dayNames[i];
185
+ const items = [
186
+ ...postedItems.filter(p => p.dateKey === dateKey),
187
+ ...scheduledItems.filter(s => s.dateKey === dateKey),
188
+ ];
189
+ days.push({ monthDay, dayName, items, dayOffset, isToday: dayOffset === 0 });
190
+ }
191
+ const mondayDate = new Date(today);
192
+ mondayDate.setDate(mondayDate.getDate() + mondayOffset);
193
+ const sundayDate = new Date(today);
194
+ sundayDate.setDate(sundayDate.getDate() + mondayOffset + 6);
195
+ const weekLabel = `${MONTHS[mondayDate.getMonth()]} ${mondayDate.getDate()} - ${MONTHS[sundayDate.getMonth()]} ${sundayDate.getDate()}`;
196
+ return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(Text, { bold: true, color: "gray", children: [' ', "WEEK: ", weekLabel] }), _jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "row", children: days.map((day, i) => {
197
+ const isSelected = day.dayOffset === selectedDay;
198
+ return (_jsxs(Box, { flexDirection: "column", width: dayWidth, borderStyle: "single", borderColor: isSelected ? 'yellow' : 'gray', children: [_jsxs(Box, { justifyContent: "space-between", paddingX: 1, children: [_jsx(Text, { bold: true, color: getDayNameColor(isSelected, day.isToday), children: day.dayName.slice(0, 3) }), _jsx(Text, { color: "gray", children: day.monthDay })] }), _jsx(Box, { flexDirection: "column", height: 5, children: day.items.length === 0 ? (_jsx(Text, { color: "gray", children: " -" })) : (day.items.slice(0, 3).map((item, j) => {
199
+ const isItemSelected = item.id === selectedId;
200
+ const isPosted = item.type === 'posted';
201
+ return (_jsxs(Text, { backgroundColor: isItemSelected ? 'yellow' : undefined, color: getCalendarItemColor(isItemSelected, isPosted), children: [isPosted ? '✓ ' : '○ ', item.id.slice(0, dayWidth - 4)] }, j));
202
+ })) })] }, i));
203
+ }) })] }));
204
+ }
205
+ /**
206
+ * Main TUI application component
207
+ * Manages state, keyboard input, and file system watching
208
+ * Keyboard shortcuts: Tab (switch tabs), arrows (navigate), Enter (action), q (quit)
209
+ */
210
+ function App() {
211
+ const { exit } = useApp();
212
+ const { stdout } = useStdout();
213
+ const [tab, setTab] = useState('drafts');
214
+ const [selectedIndex, setSelectedIndex] = useState(0);
215
+ const [scheduleMode, setScheduleMode] = useState(false);
216
+ const [selectedDay, setSelectedDay] = useState(0);
217
+ const [refreshKey, setRefreshKey] = useState(0);
218
+ const [urlInput, setUrlInput] = useState(null);
219
+ const [size, setSize] = useState({ width: stdout?.columns || 80, height: stdout?.rows || 24 });
220
+ const { postsDir, draftsDir } = getContentDirs();
221
+ // Handle terminal resize
222
+ useEffect(() => {
223
+ if (!stdout) {
224
+ return;
225
+ }
226
+ const onResize = () => setSize({ width: stdout.columns, height: stdout.rows });
227
+ stdout.on('resize', onResize);
228
+ return () => {
229
+ stdout.off('resize', onResize);
230
+ };
231
+ }, [stdout]);
232
+ // Watch filesystem for changes
233
+ useEffect(() => {
234
+ const watchers = [];
235
+ const triggerRefresh = () => setRefreshKey(r => r + 1);
236
+ if (fs.existsSync(postsDir)) {
237
+ watchers.push(fs.watch(postsDir, { recursive: true }, triggerRefresh));
238
+ }
239
+ if (fs.existsSync(draftsDir)) {
240
+ watchers.push(fs.watch(draftsDir, { recursive: true }, triggerRefresh));
241
+ }
242
+ return () => watchers.forEach(w => w.close());
243
+ }, [postsDir, draftsDir]);
244
+ const { width, height } = size;
245
+ // Memoize content parsing to avoid re-parsing on every render
246
+ // refreshKey is intentionally included to force re-parse on file system changes
247
+ // eslint-disable-next-line react-hooks/exhaustive-deps
248
+ const posts = useMemo(() => parseContent(postsDir, 'P'), [postsDir, refreshKey]);
249
+ // eslint-disable-next-line react-hooks/exhaustive-deps
250
+ const drafts = useMemo(() => parseContent(draftsDir, 'D'), [draftsDir, refreshKey]);
251
+ // Memoize sorted drafts
252
+ const sortedDrafts = useMemo(() => [...drafts].sort((a, b) => {
253
+ const dateA = a.scheduledAt || '9999-99-99';
254
+ const dateB = b.scheduledAt || '9999-99-99';
255
+ return dateA.localeCompare(dateB);
256
+ }), [drafts]);
257
+ const currentDateKey = getDateKey(selectedDay);
258
+ const currentDateDisplay = getDateDisplay(selectedDay);
259
+ const toDateKey = (dateStr) => {
260
+ if (!dateStr) {
261
+ return null;
262
+ }
263
+ const [datePart] = dateStr.split(' ');
264
+ return datePart;
265
+ };
266
+ const getFilteredItems = (allItems, isPost) => {
267
+ if (!scheduleMode) {
268
+ return allItems;
269
+ }
270
+ return allItems.filter(item => {
271
+ const dateField = isPost ? item.postedAt : item.scheduledAt;
272
+ return toDateKey(dateField) === currentDateKey;
273
+ });
274
+ };
275
+ const filteredPosts = getFilteredItems(posts, true);
276
+ const filteredDrafts = getFilteredItems(sortedDrafts, false);
277
+ const items = tab === 'posts' ? filteredPosts : filteredDrafts;
278
+ const selected = items[selectedIndex] || null;
279
+ const mainHeight = scheduleMode ? height - 15 : height - 6;
280
+ const sidebarWidth = 45;
281
+ const previewWidth = width - sidebarWidth - 2;
282
+ useInput((input, key) => {
283
+ // URL input mode
284
+ if (urlInput !== null) {
285
+ if (key.escape) {
286
+ setUrlInput(null);
287
+ return;
288
+ }
289
+ if (key.return) {
290
+ if (urlInput.trim() && selected) {
291
+ addPublishedUrl(selected, urlInput.trim());
292
+ setRefreshKey((r) => r + 1);
293
+ }
294
+ setUrlInput(null);
295
+ return;
296
+ }
297
+ if (key.backspace || key.delete) {
298
+ setUrlInput(u => (u ? u.slice(0, -1) : ''));
299
+ return;
300
+ }
301
+ if (input && !key.ctrl && !key.meta) {
302
+ setUrlInput(u => (u || '') + input);
303
+ return;
304
+ }
305
+ return;
306
+ }
307
+ // Normal mode
308
+ if (input === 'q') {
309
+ exit();
310
+ }
311
+ if (input === 's') {
312
+ setScheduleMode(m => !m);
313
+ setSelectedIndex(0);
314
+ }
315
+ if (key.tab) {
316
+ setTab(t => (t === 'posts' ? 'drafts' : 'posts'));
317
+ setSelectedIndex(0);
318
+ }
319
+ if (key.upArrow) {
320
+ setSelectedIndex(i => Math.max(0, i - 1));
321
+ }
322
+ if (key.downArrow) {
323
+ setSelectedIndex(i => Math.min(items.length - 1, i + 1));
324
+ }
325
+ if (scheduleMode && key.leftArrow) {
326
+ setSelectedDay(d => d - 1);
327
+ setSelectedIndex(0);
328
+ }
329
+ if (scheduleMode && key.rightArrow) {
330
+ setSelectedDay(d => d + 1);
331
+ setSelectedIndex(0);
332
+ }
333
+ // Enter: toggle ready/draft status for drafts, or add URL for posts
334
+ if (key.return && selected) {
335
+ if (tab === 'drafts') {
336
+ const newStatus = selected.status === 'ready' ? 'draft' : 'ready';
337
+ updateStatusLib(selected, newStatus);
338
+ setRefreshKey((r) => r + 1);
339
+ }
340
+ else if (tab === 'posts' && !selected.published?.startsWith('http')) {
341
+ setUrlInput('');
342
+ }
343
+ }
344
+ // Space: move draft to posts, or unpost post
345
+ if (input === ' ' && selected) {
346
+ if (tab === 'drafts') {
347
+ moveToPostsLib(selected);
348
+ setTab('posts');
349
+ setSelectedIndex(0);
350
+ setRefreshKey((r) => r + 1);
351
+ }
352
+ else {
353
+ moveToDrafts(selected);
354
+ setSelectedIndex(0);
355
+ setRefreshKey((r) => r + 1);
356
+ }
357
+ }
358
+ // Backspace/Delete: delete in-progress draft
359
+ if ((key.backspace || key.delete) &&
360
+ tab === 'drafts' &&
361
+ selected &&
362
+ selected.status !== 'ready') {
363
+ deleteDraft(selected);
364
+ setSelectedIndex(Math.max(0, selectedIndex - 1));
365
+ setRefreshKey((r) => r + 1);
366
+ }
367
+ });
368
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Header, { width: width }), _jsx(Tabs, { active: tab, width: width, scheduleMode: scheduleMode, currentDateDisplay: currentDateDisplay, draftsCount: filteredDrafts.length, postsCount: filteredPosts.length }), _jsxs(Box, { flexDirection: "row", height: mainHeight, children: [tab === 'drafts' ? (_jsx(DraftsList, { drafts: filteredDrafts, selectedIndex: selectedIndex, height: mainHeight, width: sidebarWidth })) : (_jsx(PostsList, { posts: filteredPosts, selectedIndex: selectedIndex, height: mainHeight, width: sidebarWidth })), _jsx(Preview, { item: selected, width: previewWidth, height: mainHeight, isPosts: tab === 'posts' })] }), scheduleMode && (_jsx(Calendar, { selectedId: selected?.id, width: width, posts: posts, drafts: sortedDrafts, selectedDay: selectedDay })), urlInput !== null && (_jsxs(Box, { borderStyle: "round", borderColor: "green", paddingX: 2, marginTop: 1, children: [_jsxs(Text, { color: "green", bold: true, children: ["Paste URL:", ' '] }), _jsx(Text, { color: "white", children: urlInput }), _jsx(Text, { color: "cyan", children: "\u258C" }), _jsx(Text, { color: "gray", children: " (Enter to confirm, Esc to cancel)" })] }))] }));
369
+ }
370
+ export function renderTUI() {
371
+ render(_jsx(App, {}));
372
+ }
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "noslop",
3
+ "version": "0.1.0",
4
+ "description": "CLI for AI assistants to manage social media posts as markdown files",
5
+ "type": "module",
6
+ "bin": {
7
+ "noslop": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "LICENSE",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "start": "tsx src/index.ts",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "test:coverage": "vitest run --coverage",
28
+ "lint": "eslint src/",
29
+ "lint:fix": "eslint src/ --fix",
30
+ "format": "prettier --write \"src/**/*.{ts,tsx}\"",
31
+ "format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
32
+ "typecheck": "tsc --noEmit",
33
+ "check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm test",
34
+ "clean": "rm -rf dist",
35
+ "prepublishOnly": "npm run build",
36
+ "prepare": "npm run build"
37
+ },
38
+ "dependencies": {
39
+ "commander": "^12.0.0",
40
+ "ink": "^5.1.0",
41
+ "ink-link": "^4.1.0",
42
+ "react": "^18.3.1"
43
+ },
44
+ "devDependencies": {
45
+ "@eslint/js": "^9.0.0",
46
+ "@types/node": "^20.0.0",
47
+ "@types/react": "^18.2.0",
48
+ "@vitest/coverage-v8": "^3.0.0",
49
+ "eslint": "^9.0.0",
50
+ "eslint-plugin-react": "^7.37.0",
51
+ "eslint-plugin-react-hooks": "^5.0.0",
52
+ "prettier": "^3.4.0",
53
+ "tsx": "^4.7.0",
54
+ "typescript": "^5.0.0",
55
+ "typescript-eslint": "^8.0.0",
56
+ "vitest": "^3.0.0"
57
+ },
58
+ "engines": {
59
+ "node": ">=18.0.0"
60
+ },
61
+ "keywords": [
62
+ "cli",
63
+ "content",
64
+ "social-media",
65
+ "tui",
66
+ "ink",
67
+ "workflow",
68
+ "terminal",
69
+ "twitter",
70
+ "x",
71
+ "content-management",
72
+ "scheduling"
73
+ ],
74
+ "author": "Ruben",
75
+ "license": "MIT",
76
+ "repository": {
77
+ "type": "git",
78
+ "url": "git+https://github.com/rubenartus/noslop.git"
79
+ },
80
+ "bugs": {
81
+ "url": "https://github.com/rubenartus/noslop/issues"
82
+ },
83
+ "homepage": "https://github.com/rubenartus/noslop#readme"
84
+ }