From 6344d519c017497c9e60757e1be72c3f596b7026 Mon Sep 17 00:00:00 2001 From: Paulo Junior Date: Tue, 11 Aug 2026 14:39:33 -0300 Subject: [PATCH 1/4] add a release skill documenting the release process --- .claude/skills/release/SKILL.md | 134 ++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 .claude/skills/release/SKILL.md diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md new file mode 100644 index 0000000..b65dd41 --- /dev/null +++ b/.claude/skills/release/SKILL.md @@ -0,0 +1,134 @@ +--- +name: release +description: Cut a tagged release of Vortex Studio - bump the version, open the develop-to-master release PR, merge it, tag master, publish the GitHub release, and merge master back into develop. Use whenever the user asks to release, cut a release, ship a version, tag a version, do a major/minor/patch release, or "do the whole release process". Covers where the version number lives and the exact branch, commit-message and tag conventions the repo already follows. +--- + +# Releasing Vortex Studio + +The canonical rules live in `CONTRIBUTING.md` under "Releases". This skill is the +executable version of them plus the details that only show up when you actually do it. + +Releases go out from `develop`. `master` is the released branch and is always tagged. + +## Pick the number + +From `CONTRIBUTING.md`: + +- **last number** (1.0.2 → 1.0.3) for fixes only +- **middle number** (1.0.2 → 1.1.0) when there are new features +- **first number** (1.0.2 → 2.0.0) only when the saved map format changes in a way + older files or older builds cannot handle + +Read the commits before deciding — do not take the user's word for "minor" if the log +shows a format change: + +```bash +git log --oneline "$(git describe --tags --abbrev=0 master)"..develop --no-merges +``` + +If the range touches the map format — `resources/js/studio/vortexProject.js`, its +round-trip tests in `scripts/project.test.mjs`, or any migration under +`database/migrations` that rewrites saved parts — stop and confirm the major bump with +the user before continuing. + +## Where the version lives + +`package.json` is the only source of truth. Nothing else hardcodes it: + +- `vite.config.js` reads `package.json` at build time into `__APP_VERSION__` +- `resources/js/studio/version.js` re-exports that as `APP_VERSION`, falling back to `'dev'` +- `MenuBar.jsx` shows it in Help → About; `StartScreen.jsx` shows it at the bottom of the start screen + +So a release edits exactly two files, and the diff is three lines: + +- `package.json` — the `"version"` key +- `package-lock.json` — the `"version"` at the top **and** the one under `packages.""` + +**Do not bump `live-editing-server/package.json`.** It has sat at 1.0.0 through every +release so far and is versioned separately. Changing that is a decision for the user, +not a step in this process. + +Do not run `npm version` — it makes a tag and a commit that do not match the conventions below. + +## The process + +Confirm the merge the release is built on actually landed on `develop` before starting. + +### 1. Tests, before anything else + +```bash +npm test # scripts/*.test.mjs +./vendor/bin/pest # tests/Feature +``` + +Both must be green. If either fails, stop and report — do not release around a failure. +Keep the counts, they go in the PR body. + +### 2. Bump on `develop` + +```bash +git checkout develop && git pull --ff-only +# edit package.json + package-lock.json +git add package.json package-lock.json +git commit -m "set the version to X.Y.Z" +git push origin develop +``` + +Commit message is exactly `set the version to X.Y.Z`, lowercase, no `v`. + +### 3. Release PR + +```bash +gh pr create --base master --head develop --title "release vX.Y.Z" --body "..." +``` + +Body: one line on why the middle/last number moved, an **In this release** list written +from the commit log in plain user-facing language, and a checklist of the steps below. + +### 4. Merge, tag, publish + +```bash +gh pr merge --merge --subject "merge develop into master for vX.Y.Z" --body "" +git checkout master && git pull --ff-only +git tag vX.Y.Z && git push origin vX.Y.Z +``` + +A **merge commit**, never squash and never rebase — the tag has to sit on the merge +commit, the way `v1.0.2` (`3e5b4a2`) and `v1.1.0` (`ce380ec`) do. Tag name carries the +`v`, the version in `package.json` does not. + +Then the GitHub release, matching the voice of the existing ones (`gh release view v1.1.0`): +a one-line summary of what kind of release it is, an **In this release** bullet list +ending with "the version bumped to X.Y.Z", an optional **Thanks** section crediting +outside contributors by handle and PR number, and a compare link last. + +```bash +gh release create vX.Y.Z --title "vX.Y.Z" --notes "..." +``` + +### 5. Merge back — do not skip + +```bash +git checkout develop && git pull --ff-only +git merge master --no-ff -m "merge master back into develop after vX.Y.Z" +git push origin develop +``` + +Without this the branches diverge and the next release PR carries stale history. +See `6051d7c` after v1.0.2 and `af6ffd9` after v1.1.0. + +### 6. Verify + +```bash +git diff origin/master origin/develop --stat # must be empty +git log --graph --oneline --all -8 +gh release list | head -3 +``` + +## Permission prompts + +`gh pr merge` and `git checkout master` have been blocked by the auto-mode permission +classifier mid-release. That leaves the release half-finished — bumped and PR'd but not +tagged. If it happens, say plainly which step is blocked and print the remaining commands +rather than probing for a way around it; the user can run them with `!` or add a Bash +permission rule. From 1c825f92af6b0635bf92a781193e474d9bf3fafe Mon Sep 17 00:00:00 2001 From: Paulo Junior Date: Tue, 11 Aug 2026 15:21:35 -0300 Subject: [PATCH 2/4] make Help > About a movable window with build, uptime and contributors It opens centred at 4:3, remembers where you drag it like the other panels, and shows the version and commit, when the last change landed, how long this build has been live, and the GitHub contributors with their avatars. The git details come from vite at build time, the uptime from a new /api/about, and the contributor list from a cached call to the GitHub API. --- .env.example | 2 + app/Http/Controllers/AboutController.php | 65 +++++++++ config/services.php | 4 + resources/css/app.css | 101 ++++++++++++-- resources/js/studio/AboutWindow.jsx | 159 +++++++++++++++++++++++ resources/js/studio/MenuBar.jsx | 17 +-- resources/js/studio/api.js | 12 ++ resources/js/studio/version.js | 4 + routes/web.php | 3 + vite.config.js | 30 +++++ 10 files changed, 373 insertions(+), 24 deletions(-) create mode 100644 app/Http/Controllers/AboutController.php create mode 100644 resources/js/studio/AboutWindow.jsx diff --git a/.env.example b/.env.example index 582d92b..b657436 100644 --- a/.env.example +++ b/.env.example @@ -67,3 +67,5 @@ VITE_APP_NAME="${APP_NAME}" VITE_LIVE_URL=ws://localhost:8787 LIVE_SECRET= + +GITHUB_REPO=N7z/vortex-studio diff --git a/app/Http/Controllers/AboutController.php b/app/Http/Controllers/AboutController.php new file mode 100644 index 0000000..1e600a3 --- /dev/null +++ b/app/Http/Controllers/AboutController.php @@ -0,0 +1,65 @@ +json([ + 'deployed_at' => $deployed, + 'deploy_uptime' => $deployed ? time() - $deployed : null, + ]); + } + + public function contributors() + { + $repo = config('services.github.repo'); + + if (! $repo) { + return response()->json(['contributors' => []]); + } + + return response()->json([ + 'contributors' => Cached::remember("about_contributors_$repo", self::CONTRIBUTOR_TTL, function () use ($repo) { + try { + $r = Http::timeout(5) + ->withHeaders(['Accept' => 'application/vnd.github+json']) + ->get("https://api.github.com/repos/$repo/contributors", ['per_page' => 30]); + + if (! $r->successful()) { + return []; + } + + return collect($r->json()) + ->filter(fn ($c) => ($c['type'] ?? '') === 'User') + ->map(fn ($c) => [ + 'name' => $c['login'], + 'avatar' => $c['avatar_url'] ?? null, + 'url' => $c['html_url'] ?? null, + 'commits' => (int) ($c['contributions'] ?? 0), + ]) + ->values() + ->all(); + } catch (\Throwable $e) { + return []; + } + }), + ]); + } + + private static function deployedAt(): ?int + { + $manifest = public_path('build/manifest.json'); + $at = is_file($manifest) ? @filemtime($manifest) : false; + + return $at === false ? null : $at; + } +} diff --git a/config/services.php b/config/services.php index 2e91d39..ba13ff6 100644 --- a/config/services.php +++ b/config/services.php @@ -22,6 +22,10 @@ 'key' => env('RESEND_API_KEY'), ], + 'github' => [ + 'repo' => env('GITHUB_REPO', 'N7z/vortex-studio'), + ], + // Shared with live-editing-server, which has no other way to know who a member is. 'live' => [ 'secret' => env('LIVE_SECRET'), diff --git a/resources/css/app.css b/resources/css/app.css index 5f9b369..4166d26 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -160,14 +160,6 @@ a.tool-btn { text-decoration: none; } } .help-close:hover { color: var(--ink); } -.about-pop { width: 300px; text-align: center; } -.about-pop .about-version { - margin: 2px 0 10px; - color: var(--dim); - font-variant-numeric: tabular-nums; -} -.about-pop a { color: var(--dim); } - .help-pop::-webkit-scrollbar { width: 8px; } .help-pop::-webkit-scrollbar-track { background: transparent; } .help-pop::-webkit-scrollbar-thumb { background: var(--line-mid); border-radius: 4px; } @@ -1044,7 +1036,8 @@ a.tool-btn { text-decoration: none; } .studio.mobile .arch-pop, .studio.mobile .team-panel, .studio.mobile .chat-panel, -.studio.mobile .stats-panel { +.studio.mobile .stats-panel, +.studio.mobile .about-panel { left: 50%; right: auto; top: 10px; @@ -1053,6 +1046,7 @@ a.tool-btn { text-decoration: none; } max-height: calc(100% - 20px); overflow-y: auto; } +.studio.mobile .about-panel { aspect-ratio: auto; } .studio.mobile .arch-head { cursor: default; } .studio.mobile .start { display: flex; flex-direction: column; } @@ -1666,6 +1660,95 @@ a.tool-btn { text-decoration: none; } .stats-panel { width: 214px; top: 46px; right: 10px; } .stats-panel .team-body { gap: 0; padding: 6px 10px 9px; } +.about-panel { + position: fixed; + top: 50%; + left: 50%; + right: auto; + transform: translate(-50%, -50%); + width: 440px; + max-width: calc(100vw - 32px); + aspect-ratio: 4 / 3; + max-height: calc(100vh - 32px); + display: flex; + flex-direction: column; + z-index: 50; +} +.about-panel .team-body { + flex: 1; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; + gap: 0; + padding: 6px 10px 9px; +} +.about-title { + font-size: 15px; + font-weight: 600; + color: var(--ink-hi); + text-align: center; + margin-top: 6px; +} +.about-version { + text-align: center; + color: var(--dim); + font-variant-numeric: tabular-nums; + margin: 2px 0 8px; +} +.about-blurb { color: var(--dim); margin-bottom: 8px; line-height: 1.5; text-align: center; } +.about-blurb a { color: var(--dim); } +.about-subject { + color: var(--dim); + margin-top: 3px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.about-people { display: flex; flex-direction: column; gap: 6px; margin-top: 3px; } +.about-person { + display: flex; + align-items: center; + gap: 8px; + font-size: 11px; + padding: 2px 4px; + margin: 0 -4px; + border-radius: 4px; + text-decoration: none; +} +a.about-person:hover { background: var(--field-alt); } +a.about-person:hover .about-name { color: var(--ink-hi); } +.about-dot { + flex: none; + width: 22px; + height: 22px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 9px; + font-weight: 600; + color: var(--ink-white); +} +.about-name { + flex: 1; + min-width: 0; + color: var(--ink-mid); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.about-bar { + flex: none; + width: 76px; + height: 4px; + border-radius: 2px; + background: var(--line); + overflow: hidden; +} +.about-face { object-fit: cover; border: 1px solid var(--line); } +.about-bar i { display: block; height: 100%; background: var(--accent); } +.about-count { flex: none; color: var(--dim); font-variant-numeric: tabular-nums; } + .stat-group { font-size: 10px; font-weight: 600; diff --git a/resources/js/studio/AboutWindow.jsx b/resources/js/studio/AboutWindow.jsx new file mode 100644 index 0000000..3546028 --- /dev/null +++ b/resources/js/studio/AboutWindow.jsx @@ -0,0 +1,159 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import useDraggable from './useDraggable'; +import { loadAbout, loadContributors } from './api'; +import { APP_VERSION, BUILD_INFO } from './version'; + +const n = (v) => (v ?? 0).toLocaleString(); + +function dur(s) { + if (s == null) return null; + const d = Math.floor(s / 86400); + const h = Math.floor((s % 86400) / 3600); + const m = Math.floor((s % 3600) / 60); + if (d) return `${d} d ${h} h`; + if (h) return `${h} h ${m} min`; + if (m) return `${m} min`; + + return `${Math.floor(s)} s`; +} + +function ago(ts) { + if (!ts) return null; + const s = Math.floor(Date.now() / 1000 - ts); + + return s < 60 ? 'just now' : `${dur(s)} ago`; +} + +const full = (ts) => (ts ? new Date(ts * 1000).toLocaleString() : undefined); + +function initials(name) { + const parts = name.split(/[\s_-]+/).filter(Boolean); + + return ((parts[0]?.[0] ?? '') + (parts.length > 1 ? parts[parts.length - 1][0] : '')).toUpperCase(); +} + +function hue(name) { + let h = 0; + for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) % 360; + + return h; +} + +function Row({ label, value, hint }) { + if (value == null || value === '') return null; + + return ( +
+ {label} + {value} +
+ ); +} + +function Face({ person }) { + const [broken, setBroken] = useState(false); + const tint = { background: `hsl(${hue(person.name)} 55% 42%)` }; + + if (!person.avatar || broken) { + return {initials(person.name)}; + } + + return ( + setBroken(true)} + /> + ); +} + +function Person({ person, top }) { + const body = ( + <> + + {person.name} + + + + {n(person.commits)} + + ); + const title = `${n(person.commits)} commits`; + + if (!person.url) return
{body}
; + + return ( + + {body} + + ); +} + +export default function AboutWindow({ onClose }) { + const { style, onPointerDown } = useDraggable('about'); + const [server, setServer] = useState(null); + const [people, setPeople] = useState(null); + const [now, setNow] = useState(() => Date.now()); + + useEffect(() => { + let live = true; + loadAbout().then((about) => live && setServer({ ...about, at: Date.now() })).catch(() => {}); + loadContributors().then((list) => live && setPeople(list)).catch(() => live && setPeople([])); + + return () => { live = false; }; + }, []); + + useEffect(() => { + const t = setInterval(() => setNow(Date.now()), 1000); + + return () => clearInterval(t); + }, []); + + const commitAt = useMemo(() => { + const iso = BUILD_INFO.commitAt; + + return iso ? Math.floor(new Date(iso).getTime() / 1000) : null; + }, []); + + const uptime = server?.deploy_uptime == null ? null : server.deploy_uptime + Math.floor((now - server.at) / 1000); + const contributors = people?.length ? people : (BUILD_INFO.contributors ?? []); + const top = contributors[0]?.commits ?? 1; + const placed = style ? { ...style, transform: 'none' } : undefined; + + return ( +
+
+ About + +
+
+

Paulin Studio

+

+ {BUILD_INFO.commit ? `Version ${APP_VERSION} · ${BUILD_INFO.commit}` : `Version ${APP_VERSION}`} +

+

+ An independent fan-made map editor that runs in your browser. Not + affiliated with playvortex.io. +

+ + + + +
+ {contributors.length ? `Contributors (${contributors.length})` : 'Contributors'} +
+ {!contributors.length ? ( + {people == null ? 'Loading...' : 'Not available.'} + ) : ( +
+ {contributors.map((c) => )} +
+ )} +
+
+ ); +} diff --git a/resources/js/studio/MenuBar.jsx b/resources/js/studio/MenuBar.jsx index 738db88..020b739 100644 --- a/resources/js/studio/MenuBar.jsx +++ b/resources/js/studio/MenuBar.jsx @@ -1,10 +1,10 @@ import { Maximize, Minimize } from 'lucide-react'; import React, { useEffect, useRef, useState } from 'react'; +import AboutWindow from './AboutWindow'; import { loadStats } from './api'; import useFullscreen, { canFullscreen } from './useFullscreen'; import { MODES, PRESETS, SCALES, SHADOW_RES, presetName } from './graphics'; import { THEMES, setTheme, useTheme } from './theme'; -import { APP_VERSION } from './version'; const SEP = { sep: true }; @@ -363,20 +363,7 @@ export default function MenuBar({ )} - {pop === 'about' && ( - <> -
setPop(null)} /> -
- setPop(null)} /> -

Paulin Studio

-

Version {APP_VERSION}

-

- An independent fan-made map editor that runs in your browser. Not - affiliated with playvortex.io. -

-
- - )} + {pop === 'about' && setPop(null)} />}
); } diff --git a/resources/js/studio/api.js b/resources/js/studio/api.js index 9933504..d061bdc 100644 --- a/resources/js/studio/api.js +++ b/resources/js/studio/api.js @@ -223,3 +223,15 @@ export async function loadStats() { if (!r.ok) throw new Error('failed to load stats'); return r.json(); } + +export async function loadAbout() { + const r = await fetch('/api/about'); + if (!r.ok) throw new Error('failed to load about'); + return r.json(); +} + +export async function loadContributors() { + const r = await fetch('/api/about/contributors'); + if (!r.ok) throw new Error('failed to load contributors'); + return (await r.json()).contributors ?? []; +} diff --git a/resources/js/studio/version.js b/resources/js/studio/version.js index 0beee94..c6f2135 100644 --- a/resources/js/studio/version.js +++ b/resources/js/studio/version.js @@ -1,6 +1,10 @@ // vite fills this in from package.json, so a test importing this file has nothing export const APP_VERSION = typeof __APP_VERSION__ === 'undefined' ? 'dev' : __APP_VERSION__; +const NO_BUILD = { commit: '', commitAt: null, subject: '', contributors: [], builtAt: null }; + +export const BUILD_INFO = typeof __BUILD_INFO__ === 'undefined' ? NO_BUILD : __BUILD_INFO__; + const POLL_MS = 5 * 60_000; const MIN_GAP_MS = 60_000; diff --git a/routes/web.php b/routes/web.php index 12a05cd..ae536ba 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,5 +1,6 @@ middleware('throttle:30,1'); +Route::get('/api/about', [AboutController::class, 'info'])->middleware('throttle:30,1'); +Route::get('/api/about/contributors', [AboutController::class, 'contributors'])->middleware('throttle:30,1'); Route::get('/api/thumbs/{key}.webp', [MapController::class, 'thumb'])->where('key', '[a-f0-9]{32}'); Route::middleware('throttle:60,1')->group(function () { diff --git a/vite.config.js b/vite.config.js index 3782aba..dcf993c 100644 --- a/vite.config.js +++ b/vite.config.js @@ -2,13 +2,43 @@ import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import react from '@vitejs/plugin-react'; import { readFileSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; const { version } = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')); +const git = (...args) => { + try { + return execFileSync('git', args, { + cwd: import.meta.dirname, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + } catch { + return ''; + } +}; + +const [commit = '', commitAt = '', subject = ''] = git('log', '-1', '--format=%h%n%cI%n%s').split('\n'); + +const contributors = git('shortlog', '-sn', '--no-merges', 'HEAD') + .split('\n') + .map((line) => line.trim().match(/^(\d+)\s+(.+)$/)) + .filter(Boolean) + .map(([, commits, name]) => ({ name, commits: Number(commits) })); + +const buildInfo = { + commit, + commitAt: commitAt || null, + subject, + contributors, + builtAt: new Date().toISOString(), +}; + export default defineConfig({ root: import.meta.dirname, define: { __APP_VERSION__: JSON.stringify(version), + __BUILD_INFO__: JSON.stringify(buildInfo), }, plugins: [ laravel({ From 3cd3637d44e01ce1fb9a9e24cb4892cfa9b8b230 Mon Sep 17 00:00:00 2001 From: Paulo Junior Date: Tue, 11 Aug 2026 15:22:02 -0300 Subject: [PATCH 3/4] keep people signed in across browser restarts Registering and logging in now issue a remember cookie, and the session lifetime goes from two hours to thirty days, so building does not get interrupted by a sign-in that quietly expired. --- .env.example | 2 +- app/Http/Controllers/AccountController.php | 2 +- resources/js/studio/api.js | 2 +- tests/Feature/AccountTest.php | 13 +++++++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index b657436..97afb35 100644 --- a/.env.example +++ b/.env.example @@ -28,7 +28,7 @@ DB_CONNECTION=sqlite # DB_PASSWORD= SESSION_DRIVER=database -SESSION_LIFETIME=120 +SESSION_LIFETIME=43200 SESSION_ENCRYPT=false SESSION_PATH=/ SESSION_DOMAIN=null diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 8bdb49d..113c441 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -85,7 +85,7 @@ public function register(Request $request) 'password' => $data['password'], ]); - return $this->signIn($request, $user); + return $this->signIn($request, $user, true); } public function login(Request $request) diff --git a/resources/js/studio/api.js b/resources/js/studio/api.js index d061bdc..634e40a 100644 --- a/resources/js/studio/api.js +++ b/resources/js/studio/api.js @@ -25,7 +25,7 @@ export const liveToken = (map = null, team = null) => { .then((d) => d.token ?? null).catch(() => null); }; export const register = (fields) => account('/register', fields); -export const login = (fields) => account('/login', fields); +export const login = (fields) => account('/login', { ...fields, remember: true }); export const logout = () => account('/logout', {}); export async function listMaps() { diff --git a/tests/Feature/AccountTest.php b/tests/Feature/AccountTest.php index f195e25..2f54f29 100644 --- a/tests/Feature/AccountTest.php +++ b/tests/Feature/AccountTest.php @@ -3,6 +3,7 @@ use App\Http\Controllers\MapController; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; uses(RefreshDatabase::class); @@ -44,6 +45,18 @@ function account(array $over = []): array $this->postJson('/account/login', ['email' => 'p@example.com', 'password' => 'correct horse'])->assertOk(); }); +it('issues a remember cookie so a sign-in outlives the session', function () { + $this->postJson('/account/register', account()) + ->assertOk()->assertCookie(Auth::guard('web')->getRecallerName()); + + $this->postJson('/account/logout'); + + $this->postJson('/account/login', ['email' => account()['email'], 'password' => account()['password'], 'remember' => true]) + ->assertOk()->assertCookie(Auth::guard('web')->getRecallerName()); + + expect(User::where('email', account()['email'])->value('remember_token'))->not->toBeNull(); +}); + it('issues a live token only to a signed-in user with a secret configured', function () { config(['services.live.secret' => 'shared-with-laravel']); From 3fde58642dad8803a5d24a7aa58f4b658daa941a Mon Sep 17 00:00:00 2001 From: Paulo Junior Date: Tue, 11 Aug 2026 15:22:32 -0300 Subject: [PATCH 4/4] set the version to 1.2.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4bbf4df..7578b9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "studio", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "studio", - "version": "1.1.0", + "version": "1.2.0", "license": "AGPL-3.0-or-later", "dependencies": { "@codemirror/autocomplete": "^6.20.3", diff --git a/package.json b/package.json index 5e5d5b9..f4c6447 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "wasmoon": "^1.16.0" }, "name": "studio", - "version": "1.1.0", + "version": "1.2.0", "description": "

\"Laravel

", "main": "vite.config.js", "directories": {