开发者
Nuxt 4.5: Experimental SSR Streaming, Vite 8 and an Rsbuild-Powered Rspack Builder
Nuxt has released version 4.5, featuring updates such as a switch to Vite 8, a new Rspack 2 builder, and experimental SSR streaming. This streaming enhances Time to First Byte by flushing the HTML shell instantly. The release also includes a stable error code system and new composables, alongside important upgrade instructions for developers moving from earlier versions. By Daniel Curtis
AI 资讯
useEditorContext composable in n8n codebase.
In this article, we review useEditorContext in n8n codebase. You will learn: Composables in Vue useEditorContext as composable in n8n. Composables in Vue In the context of Vue applications, a "composable" is a function that leverages Vue's Composition API to encapsulate and reuse stateful logic. When building frontend applications, we often need to reuse logic for common tasks. For example, we may need to format dates in many places, so we extract a reusable function for that. This formatter function encapsulates stateless logic: it takes some input and immediately returns expected output. There are many libraries out there for reusing stateless logic - for example lodash and date-fns , which you may have heard of. By contrast, stateful logic involves managing state that changes over time. A simple example would be tracking the current position of the mouse on a page. In real-world scenarios, it could also be more complex logic such as touch gestures or connection status to a database. This is just like React Hooks. Learn more about Composables . useEditorContext as composable in n8n. Now that we understand what a composable is in Vue.js, btw, n8n editor-ui is written in Vue, let's understand how useEditorContext is used. Below is a comment I picked from useEditorContext.ts file. /** * Per - editor host overrides for the current editor context . * * Editor hosts ( e . g . the Instance AI artifact preview ) scope their embedded * editor by providing ` EditorEnabledFeaturesKey ` - the capabilities the host * supersedes . AI features can only be restricted: an explicit ` false ` turns one * off , while omitted ( or ` true `) features fall back to their store values . * ` readOnly ` is a direct flag - ` true ` forces the canvas read - only . When no host * provides the key , AI features fall back to their store values and the canvas * is editable (` readOnly ` is ` false `) . * ` executionSuccessToasts ` / ` executionErrorToasts ` are direct flags too - each * ` true `
AI 资讯
How to Convert PDF to Word in the Browser with Vue 3 and pdf-lib
Converting PDF to Word seems straightforward, but the reality is more complex. PDF stores text as character coordinates, while Word uses structured paragraphs. Bridging this gap requires careful text extraction and order reconstruction. Here's how to build a browser-based PDF to Word converter with Vue 3 and pdf-lib . The challenge: PDF vs Word PDF is a presentation format — text is positioned precisely on the page. Word is an editing format — text flows in paragraphs with styles. Converting between them means: Extracting text from PDF coordinates Reconstructing reading order Generating structured DOCX output The stack Vue 3 with Composition API pdf-lib for PDF parsing docx for Word document generation Vite for bundling The core implementation < script setup lang= "ts" > import { ref } from ' vue ' import { PDFDocument } from ' pdf-lib ' import { Document , Paragraph , TextRun } from ' docx ' const file = ref < File | null > ( null ) const processing = ref ( false ) const result = ref < Blob | null > ( null ) async function convertPdfToWord () { if ( ! file . value ) return processing . value = true const arrayBuffer = await file . value . arrayBuffer () const pdf = await PDFDocument . load ( arrayBuffer ) const pages = pdf . getPages () const allChunks : TextChunk [] = [] for ( const page of pages ) { const textContent = await page . getTextContent () for ( const item of textContent . items ) { allChunks . push ({ text : item . text , x : item . transform [ 4 ], y : item . transform [ 5 ], size : item . size }) } } // Sort by reading order const sorted = sortByReadingOrder ( allChunks ) // Generate DOCX const doc = new Document ({ sections : [{ properties : {}, children : sorted . map ( chunk => new Paragraph ({ children : [ new TextRun ( chunk . text )] }) ) }] }) const blob = await doc . pack () result . value = blob processing . value = false } interface TextChunk { text : string x : number y : number size : number } function sortByReadingOrder ( chunks : TextCh
AI 资讯
Perry Mason in: The Case of the Drifting Timer
Perry Mason in: The Case of the Drifting Timer Opening Statement You need a reactive "current time" in your Vue 3 app. A schedule grid with a red line showing "now." A live clock. A dashboard that updates every minute. Every Vue developer reaches for setInterval first. It works. But "works" and "works well" are different things. This is the story of taking a naive timer from "it ticks" to production-grade — and the four iterations it took to get there. The prosecution calls four exhibits. Let's begin. Exhibit A: The Memory Leak const currentTime = ref ( new Date ()) onMounted (() => { setInterval (() => { currentTime . value = new Date () }, 60000 ) }) It works. Sort of. The defense rests — but the prosecution is just getting started. Exhibits of negligence: The interval is never cleared. When the component unmounts, the timer keeps firing every 60 seconds forever — updating a ref nothing reads anymore, and holding its closure (and everything the ref references) in memory for the lifetime of the page. Silent. Invisible. The kind of leak that shows up in production after a user navigates around your app for 20 minutes. Exhibit B: Component-Only Cleanup const currentTime = ref ( new Date ()) let timeInterval = null onMounted (() => { currentTime . value = new Date () timeInterval = setInterval (() => { currentTime . value = new Date () }, 60000 ) }) onUnmounted (() => { if ( timeInterval ) clearInterval ( timeInterval ) }) Now we clean up. The interval is stored in a variable, cleared on unmount. A step forward. But onUnmounted has a scope limitation worth understanding: The limitation: onUnmounted only works inside components. If someone calls this logic from a Pinia store or outside a component's setup() context, onUnmounted never fires. The timer leaks silently. (Composables called synchronously during setup() are fine — Vue's docs recommend exactly that. The problem is when there's no component instance at all.) The timer fires 60 seconds after load , not at the t
开发者
State Management in Front-end Web Development: Mutators
Libraries like Valtio and Pinia for Vue use a mutator pattern instead of the actions, dispatch, and...
AI 资讯
Why pasted text keeps breaking search and formatting (and the regexes I ended up using to clean it)
I kept running into a boring problem that was harder to debug than it should have been: text that looked normal, but behaved wrong the moment I pasted it into a CMS, a spreadsheet, or a code comment. Search would fail. Line breaks would get weird. A heading copied from ChatGPT would drag Markdown markers along with it. Sometimes the only visible clue was that the punctuation felt slightly "off." What finally made this manageable wasn't some big NLP trick. It was going back to the dumb, reliable layer: exact character matching. The tool I built for this is basically a pile of small, deterministic cleanups for the specific junk that copied text tends to accumulate — full-width punctuation mixed into ASCII, invisible Unicode code points, curly quotes, em dashes, leftover Markdown, and whitespace noise. The most useful part is the invisible-character scan, not the cleaning The piece I trust most in the whole component is the part that explicitly names which invisible characters it cares about, then counts them by code point. It's not doing a vague "this text seems suspicious" pass. It has a hard-coded inventory: const invisibleDefs = [ { key : " zwsp " , codes : [ 0x200b ] }, { key : " zwnj " , codes : [ 0x200c ] }, { key : " zwj " , codes : [ 0x200d ] }, { key : " bomZwnbsp " , codes : [ 0xfeff ] }, { key : " wordJoiner " , codes : [ 0x2060 ] }, { key : " softHyphen " , codes : [ 0x00ad ] }, { key : " bidiMarks " , codes : [ 0x200e , 0x200f , 0x202a , 0x202b , 0x202c , 0x202d , 0x202e ] }, ]; const codesToRegex = ( codes ) => new RegExp ( `[ ${ codes . map (( c ) => " \\ u " + c . toString ( 16 ). padStart ( 4 , " 0 " )). join ( "" )} ]` , " g " ); const analyzeInvisible = ( str ) => { const breakdown = invisibleDefs . map (( def ) => ({ key : def . key , count : ( str . match ( codesToRegex ( def . codes )) || []). length , })); const total = breakdown . reduce (( sum , row ) => sum + row . count , 0 ); return { breakdown , total }; }; I like this because it's brutall
AI 资讯
The hard part of batch date conversion isn't formatting — it's deciding what `01/02/2024` means
I used to think a bulk date converter was basically a dropdown wrapped around a date library. Paste a bunch of rows, pick YYYY-MM-DD , done. Then you look at real exports from spreadsheets, CRMs, logs, and old internal tools and realize the problem isn't "formatting" at all. It's triage. Some rows are obvious. Some are malformed. Some have month names. Some came from a CSV with five unrelated columns. And then there's the classic cursed input: 01/02/2024 , which is either January 2 or February 1 depending on who produced the file. The Vue component behind this tool is interesting because it doesn't pretend that ambiguity goes away if you call the right parser. It models that ambiguity explicitly. It starts by assuming uploaded files are messy, not clean One thing I liked in the source is that it doesn't treat file input as a single happy path. If you upload a TXT file, it works line by line. If the upload looks CSV-ish, it switches into a tiny parser and then tries to figure out which column is actually the date column. The CSV split logic is manual instead of using a naive line.split(",") , which matters because quoted commas are a real thing in exports: const splitCsvLine = ( line ) => { const result = []; let cur = "" ; let inQuotes = false ; for ( let i = 0 ; i < line . length ; i ++ ) { const ch = line [ i ]; if ( ch === ' " ' ) { if ( inQuotes && line [ i + 1 ] === ' " ' ) { cur += ' " ' ; i ++ ; } else { inQuotes = ! inQuotes ; } } else if ( ch === " , " && ! inQuotes ) { result . push ( cur ); cur = "" ; } else { cur += ch ; } } result . push ( cur ); return result . map (( s ) => s . trim ()); }; After that, it doesn't ask the user to map columns immediately. It scores each column by counting how many cells look like dates, then auto-selects the best candidate: for ( let c = 0 ; c < maxCols ; c ++ ) { const count = dataRows . filter (( r ) => isLikelyDateCell ( r [ c ])). length ; columns . push ({ index : c , header : headerRow ? headerRow [ c ] : "" }); i
AI 资讯
Has anyone migrated a large AngularJS 1.8 application to Vue 3 with JavaScript?
Hi everyone, I’m currently working on a large admin portal built with AngularJS 1.8, and we’re considering migrating it to Vue 3. I’ve started learning Vue 3 and I’m trying to understand the best approach for a real-world migration. Has anyone here actually migrated an AngularJS 1.x / 1.8 application to Vue 3 using JavaScript (not TypeScript)? I’d especially like to hear about: How you planned the migration Whether you migrated gradually or rewrote the application How you handled the existing AngularJS components/modules How you structured the new Vue 3 application Any problems you encountered during the migration Whether you used a hybrid approach where AngularJS and Vue coexist temporarily Any resources, articles, or examples you found useful The application is fairly large, so I’m particularly interested in experiences from people who have done this in a real production project, rather than starting a small project from scratch. Any advice or lessons learned would be greatly appreciated!
AI 资讯
Supabase in Vue Made Simple
Supabase has become one of the most popular choices for building modern web applications. It gives you: PostgreSQL database Authentication Realtime subscriptions Storage Edge Functions TypeScript support The official Supabase JavaScript client already makes it relatively easy to use these features from a Vue application. But integrating Supabase into a Vue application usually means creating a client and then making it available throughout your application. This is where the new @supabase-community/vue-supabase package comes in. The package provides a Vue-friendly integration for Supabase, allowing you to access your Supabase client through useSupabaseClient() while keeping the familiar Supabase API. You can check out the package on GitHub here: https://github.com/supabase-community/vue-supabase In this article, we'll explore: What @supabase-community/vue-supabase is How to install and configure it How to query your database How to use TypeScript with it How to handle authentication How to use Supabase Realtime How to structure Supabase logic using Vue composables What security considerations you need to remember Let's dive in. 🤔 What Is @supabase-community/vue-supabase ? @supabase-community/vue-supabase is a Vue integration for Supabase that provides a convenient way to access the Supabase client inside your Vue application. The main API you'll use is: import { useSupabaseClient } from ' @supabase-community/vue-supabase ' const supabase = useSupabaseClient () Once you have the client, you can use the standard Supabase API: const { data , error } = await supabase . from ( ' profiles ' ) . select ( ' * ' ) This is important because the package doesn't introduce a completely new way of working with Supabase. You still use the APIs you're familiar with: supabase . from () supabase . auth supabase . channel () supabase . storage The package mainly provides the Vue integration layer around them. 🟢 Installing and Configuring the Package The package can be installed with: n
AI 资讯
Kitchen-Sune: A Community Cookbook
This is a submission for Frontend Challenge - Comfort Food Edition, Perfect Landing What I Built This is a blast from the past, but it's so lovely I couldn't resist submitting it for this challenge. Kitchen-Sune is a community-driven international recipe book built in collaboration with Front-End Foxes members when we pivoted our nonprofit's efforts from in-person workshops to an international online boot camp during the pandemic. It brings together comfort food recipes from around the globe into a sleek, accessible, and user-friendly Vuepress web application where food lovers and developers alike can discover new dishes. We were happy to host recipes from Ukraine, Kenya, Nigeria, and everywhere in between. Between Jalebi Babies, Moin-Moin, Puff Puff, and Strawberry Mush, we've got you covered for comfort food! Demo Live Demo: Kitchen-Sune App GitHub Repo: https://github.com/FrontEndFoxes/kitchen-sune Check out a preview of the recipe book (this recipe for maple syrup candy came with a video): Journey Revisiting and showcasing this project was a nostalgic process. Working on a community recipe project taught me the importance of building inclusive, easy-to-navigate web interfaces for diverse content as an educatioal tool. We used to use this repo as a way to train boot camp enrollees in how to use GitHub and make a PR to a repo. What I Learned: No matter where we are in our journey, food brings us together. What I'm Proud Of: How timeless and clean the aesthetic remains, making it easy for anyone to find a warm, cozy meal to cook or a snack to throw together (Pandemic Cookies, anyone?). What's Next: Let's keep going! Add your recipe via a PR to the GitHub repo. And I'd love to have more photos to show off your work.
AI 资讯
Perry Mason in: The Case of the Drifting Timer
Perry Mason in: The Case of the Drifting Timer Opening Statement You need a reactive "current time" in your Vue 3 app. A schedule grid with a red line showing "now." A live clock. A dashboard that updates every minute. Every Vue developer reaches for setInterval first. It works. But "works" and "works well" are different things. This is the story of taking a naive timer from "it ticks" to production-grade — and the four iterations it took to get there. The prosecution calls four exhibits. Let's begin. Exhibit A: The Memory Leak const currentTime = ref ( new Date ()) onMounted (() => { setInterval (() => { currentTime . value = new Date () }, 60000 ) }) It works. Sort of. The defense rests — but the prosecution is just getting started. Exhibits of negligence: The interval is never cleared. When the component unmounts, the timer keeps firing every 60 seconds forever — updating a ref nothing reads anymore, and holding its closure (and everything the ref references) in memory for the lifetime of the page. Silent. Invisible. The kind of leak that shows up in production after a user navigates around your app for 20 minutes. Exhibit B: The Cleanup That Failed const currentTime = ref ( new Date ()) let timeInterval = null onMounted (() => { currentTime . value = new Date () timeInterval = setInterval (() => { currentTime . value = new Date () }, 60000 ) }) onUnmounted (() => { if ( timeInterval ) clearInterval ( timeInterval ) }) Now we clean up. The interval is stored in a variable, cleared on unmount. A step forward — but the prosecution has three more objections: Further evidence: This only works inside components. If someone calls this logic from a Pinia store or outside a component's setup() context, onUnmounted never fires. The timer leaks silently. (Composables called synchronously during setup() are fine — Vue's docs recommend exactly that. The problem is when there's no component instance at all.) The timer fires 60 seconds after load , not at the top of the minute
AI 资讯
I Was Tired of Losing Disk Space to node_modules - So I Built ArtifactSweep
Being a developer, we all create many projects for learning, work, and experiments. Over time my machine started filling up — not with source code, but with generated junk : node_modules target dist / build framework caches like .next , .angular , .nuxt and more of the same across every cloned repo Every few months I would hunt folders manually, delete something, free a few GB, then the same problem would come back. Only learning about “clean your disk” tips doesn’t help much. Building something for the problem does. So I ended up building ArtifactSweep — a small open-source tool for this everyday developer issue. The real problem As developers we regenerate these folders all the time: npm install cargo build ng build They are not our source of truth. But they sit on the SSD for months. The painful part is not only size. It is: Finding them across many project roots Knowing how big they are before delete Not deleting the wrong folder by mistake I wanted something that could: Scan a folder tree Show sizes Let me clean with more control Work on my day-to-day machines (Windows, Linux, Mac) Step 1: Start with a CLI I started with the command line first. Why CLI? Fast to build and test Fits terminal-first workflow Easy to script and share The CLI is called sweep . Basic usage: # Safe: only list junk under a path sweep scan . # Preview deletes sweep clean . --dry-run # Delete sweep clean . On one of my project folders alone, it reclaimed nearly 5 GB . That was enough validation: this is not a fake problem. Every active developer hits it. Step 2: Then came the desktop app CLI is great when you already know the path and trust dry-run. But sometimes I wanted to: See a list of folders and sizes Filter by type Confirm before delete Click through without remembering flags So I added a desktop app on top of the same idea (same cleanup job, different UI). Flow is simple: Choose folder Scan Review results (and filters if needed) Clean with confirmation If you like GUIs for this ki
AI 资讯
The Case of the Lying Clock: 5 Vue Mysteries Solved
Every detective has their cold cases. These are mine — five Vue concepts that confused me until I investigated them properly. Grab your magnifying glass. Case #1: The Lying Clock Imagine you set an alarm to go off every 60 seconds. You press start at 10:00:00. First alarm: 10:01:00 — perfect Second alarm: 10:02:00 — still good But your phone is also doing other things: checking email, refreshing weather, running background tasks. Sometimes it fires the alarm a tiny bit late. Third alarm: 10:03:01 (1 second late) Fourth alarm: 10:04:02 (2 seconds off now) Five hours later: your alarm fires at 15:05:12 when it should fire at 15:05:00 That gap growing bigger over time — that's drift . The timer slowly slides away from where it should be. Why Does This Happen? JavaScript runs on a single thread — it can only do one thing at a time. When a timer is supposed to fire, the browser puts it in a queue. But if the thread is busy doing something else, the timer waits. The MDN documentation for setTimeout lists several reasons timers fire late: Nested timeouts are throttled to a minimum of 4ms after 5 levels of nesting (per the HTML5 spec ) Background tabs are throttled to a maximum of once per second ( MDN : "timeouts are throttled to firing no more often than once per second (1000 ms) in inactive tabs") Chrome 88+ introduced intensive throttling for hidden pages: timers that have been hidden for more than 5 minutes are checked only once per minute Tracking scripts in Firefox get even more aggressive throttling: 10 second minimum in background tabs Does This Happen on New Devices Too? Yes, but less. Modern devices are faster, so the delay per tick is smaller — maybe 1-2 milliseconds instead of 10-20. But over hours, even 1ms per tick adds up. And background tab throttling happens on every device, no matter how fast — it's a browser policy, not a hardware limitation. Is This Common Knowledge? It's the kind of thing you learn when your boss says "why does the clock on our dashboa
产品设计
Nuxt 4.5 SSR Streaming Is Kind Of A Big Deal
Nuxt 4.5 launched last month and it's really neat. One of my most favorite features is the...
AI 资讯
Static File Caching in Nuxt: An Easy and Practical Strategy
Lighthouse kept warning me about inefficient cache lifetimes, even though I had already added caching for my static files. The missing piece was Nuxt Image and its generated /_ipx URLs . In this post, I’ll share the simple caching setup I use for Nuxt build files, public assets, and optimized images without risking stale content after deployment. The basic rule is simple: Cache files aggressively when changing the file also changes its URL. Be more careful when the same URL can serve different content later. You have probably seen the same Lighthouse warning I have: Use efficient cache lifetimes. Browser caching for static files is usually straightforward. You add a Cache-Control header, choose a reasonable lifetime, and the browser avoids downloading the same files again on every visit. However, in a Nuxt application, not every static-looking file should use the same caching policy. Nuxt build files are automatically versioned. Files inside public/ usually are not. Nuxt Image also creates transformed image URLs under /_ipx , which need their own cache rule. In this post, I’ll go through the setup I use, including the Nuxt Image rule that was missing during my latest Lighthouse audit. The simple caching rule The most important question is not whether a file is an image, font, or JavaScript file. The important question is: Will the URL change when the file changes? When the answer is yes, you can safely cache the file for a very long time. When the answer is no, you should use a shorter cache lifetime. Otherwise, visitors may continue seeing an old version after you deploy an update. What the cache directives mean Here are the main directives used in this setup: public allows browsers and shared caches such as CDNs to store the response. max-age controls how long the browser considers the file fresh. s-maxage controls how long shared caches such as Cloudflare consider it fresh. immutable tells the browser that the file is not expected to change while that URL exists.
AI 资讯
How to Split PDF by File Size in the Browser with Vue 3 and pdf-lib
Splitting a PDF by file size is one of the most practical but technically tricky operations. Unlike splitting by page count (simple math) or bookmarks (tree traversal), size-based splitting requires estimating and controlling the output size of each chunk — and PDFs don't have a simple "size per page" property. Here's how to build a browser-based PDF splitter that respects file size constraints. The challenge PDFs are notoriously unpredictable in terms of size. Two PDFs with the same number of pages can differ by 10x in file size depending on: Image resolution and compression Font embedding Color space (RGB vs. CMYK) Content complexity (vector graphics vs. scanned images) This means you can't calculate split points with simple arithmetic. You need to estimate, test, and adjust . The stack Vue 3 with Composition API pdf-lib for PDF manipulation Vite for bundling The core implementation The approach is greedy accumulation with size estimation : < script setup lang= "ts" > import { ref } from ' vue ' import { PDFDocument } from ' pdf-lib ' const file = ref < File | null > ( null ) const targetSizeMB = ref < number > ( 10 ) const compression = ref < ' none ' | ' low ' | ' high ' > ( ' low ' ) const splitting = ref ( false ) const progress = ref ( 0 ) const progressTotal = ref ( 0 ) const results = ref < Record < string , Uint8Array >> ({}) async function splitBySize () { if ( ! file . value ) return splitting . value = true const arrayBuffer = await file . value . arrayBuffer () const pdf = await PDFDocument . load ( arrayBuffer ) const totalPages = pdf . getPageCount () const targetBytes = targetSizeMB . value * 1024 * 1024 const outputFiles : Array < { name : string ; data : Uint8Array } > = [] let currentPdf = await PDFDocument . create () let currentSize = 0 let pageNum = 0 for ( let i = 0 ; i < totalPages ; i ++ ) { progressTotal . value = totalPages progress . value = i + 1 // Try adding this page try { const [ copiedPage ] = await currentPdf . copyPages ( pdf , [
AI 资讯
How to Repair Corrupted PDFs in the Browser with Vue 3 and pdf-lib
A corrupted PDF is one of the most frustrating file problems. You have important content inside, but the document won't open, opens with garbled text, or shows missing pages. The file might be damaged from a bad download, a converter error, or a storage glitch. Recovering content from a broken PDF doesn't require complex forensic tools. Often, the individual pages are still readable — it's the document's structure (cross-reference tables, object streams) that's damaged. By extracting pages one by one into a fresh PDF, we can bypass the structural corruption. Here's how to build a browser-based PDF repair tool with Vue 3 and pdf-lib . The repair strategy The core insight: PDF structure and page content are somewhat independent . A PDF can have a broken cross-reference table or missing trailer objects, but the actual page content streams may still be perfectly readable. The repair approach: Load the damaged PDF and attempt to read each page For each successfully read page, copy it to a new PDF document Discard unreadable pages (they're lost anyway) Save the new document This is fundamentally different from "fixing" the original PDF. We're extracting what we can and rebuilding from the ground up. The stack Vue 3 with Composition API pdf-lib for PDF reading and page extraction Vite for bundling The core implementation < script setup lang= "ts" > import { ref } from ' vue ' import { PDFDocument } from ' pdf-lib ' const file = ref < File | null > ( null ) const totalPages = ref ( 0 ) const recoveredPages = ref ( 0 ) const repairing = ref ( false ) const result = ref < Uint8Array | null > ( null ) const error = ref < string | null > ( null ) async function repairPdf () { if ( ! file . value ) return repairing . value = true error . value = null try { const arrayBuffer = await file . value . arrayBuffer () const damaged = await PDFDocument . load ( arrayBuffer , { ignoreEncryption : true , updateMetadata : false , }) totalPages . value = damaged . getPageCount () const resu
AI 资讯
Typing Vue 3 provide/inject Without Losing Autocomplete
Strict prop types and typed emits get most of the attention in Vue 3 + TypeScript setups, but provide / inject is where type safety quietly falls apart if you use the API the way the docs show it by default. inject() without a type hint returns unknown , which means every consumer of an injected value either casts it blindly or loses autocomplete entirely — and a typo in the injection key becomes a runtime undefined instead of a compile-time error. The Default Setup Is Untyped by Construction The naive version compiles, but gives you nothing: // Provider provide ( ' theme ' , currentTheme ); // Consumer const theme = inject ( ' theme ' ); // type: unknown Nothing here catches a typo in the key string, and nothing tells the consumer what shape theme actually has. Both problems come from using a plain string as the injection key. InjectionKey Fixes Both Problems at Once Vue exports an InjectionKey<T> type specifically for this. Define it once, typed, and both provide and inject become fully type-checked against the same symbol: // keys.ts import type { InjectionKey } from ' vue ' ; export interface Theme { mode : ' light ' | ' dark ' ; accentColor : string ; } export const ThemeKey : InjectionKey < Theme > = Symbol ( ' theme ' ); // Provider import { ThemeKey } from ' ./keys ' ; provide ( ThemeKey , { mode : ' dark ' , accentColor : ' #4f46e5 ' }); // Consumer import { ThemeKey } from ' ./keys ' ; const theme = inject ( ThemeKey ); // type: Theme | undefined The | undefined in that last type isn't a quirk — it's inject being honest that a consumer might render without a matching provider above it in the tree, which is a real runtime possibility TypeScript is right to force you to handle. Handling the undefined Case Without Littering ?. Everywhere The common mistake is providing a default value to silence the undefined type instead of actually checking for it: const theme = inject ( ThemeKey , { mode : ' light ' , accentColor : ' #000 ' }); // default masks missing pro
AI 资讯
A CSV Viewer That Never Uploads Your Data
I just wanted to open a CSV file quickly. Instead, I got: Slow spreadsheet apps Online tools that upload my data Way too much friction So I defined a simple goal: Fast and frictionless Fully local (no uploads) Spreadsheet-like experience That’s what I built. What you can do with it Open CSV files instantly (drag & drop, no setup) Search and filter large datasets in seconds Edit data like a spreadsheet Export exactly what you see Why this matters Everything happens locally in your browser: Your data never leaves your device No account required No tracking, no storage Built with client-side JavaScript — no backend involved. Just open, edit, and export. How it works Built with client-side JavaScript — no backend involved. Spreadsheet-Like Editing Click a cell to select it, then press Enter or double-click to start editing. Press Enter again to save the value and move to the cell below. Use Option + Enter on macOS or Alt + Enter on Windows to insert a line break. You can also drag the small fill handle to copy a value across multiple cells. Export the Current View The exported CSV reflects the current table state, including: Search results Sorting order Visible columns Saved cell edits Try it yourself Open a CSV file, edit a few cells, and export it — all without uploading anything. 👉 https://csv-open.github.io/ No signup. No upload. Just works. Handles CSV files up to 25MB Supports multiple languages
AI 资讯
I built a browser-based pixel-art & animation editor with Vue, Laravel and AI
I'm a solo dev, and for the past few months I've been building Pixanima — a pixel-art and animation editor that runs entirely in the browser, with an optional AI assistant baked in. It just launched, and I wanted to share the parts that were technically interesting: making a general image model output clean pixel art, an atomic credit system, AI inbetweening for animation, and why the whole business model falls out of one architectural fact. What it is Draw pixel art with layers, groups and effects, animate it on a frame timeline with onion-skin, and export to GIF / sprite sheets / PNG — all client-side, no install. On top of that, an AI assistant turns a text prompt into sprites, seamless tiles and palettes, re-poses characters, and generates in-between animation frames. The frontend is Vue 3 driving an HTML canvas; the backend is Laravel 12 (PHP 8.4) . Here's what I learned. Everything runs in the browser — and that decided the business model The entire editor is client-side. Projects live in IndexedDB ; nothing is uploaded. That's great for privacy and speed, but it has a consequence a lot of people miss: you cannot meaningfully gate a client-side feature. If drawing, layers and export all run in the user's browser, any "pro" paywall around them is both unenforceable and, honestly, hostile to a price-sensitive hobbyist community. So I flipped it: the editor is 100% free, forever . The only paid thing is AI — because AI is the only part with a real marginal cost, and it requires a backend (which conveniently also protects the code that costs money to run). More on that below. Making a general model output clean pixel art The naive approach — prompt a diffusion model with "pixel art, 16 colors" — gives you pixel-art-ish mush: anti-aliased edges, hundreds of colors, no real grid. Useless as an actual sprite. The fix is a post-processing pipeline. The model just produces raw input; the "pixel art" is made deterministically afterward with PHP's GD: 1. Generate a norma