From a721e26e45bc272f09f4cead6e134136ae41a732 Mon Sep 17 00:00:00 2001 From: cjun Date: Mon, 21 Sep 2026 22:14:58 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E5=8F=B0=E7=AE=A1=E7=90=86=E9=87=8D?= =?UTF-8?q?=E5=A1=91=20+=20=E4=B8=BB=E9=A2=98=20/=20=E7=9A=AE=E8=82=A4?= =?UTF-8?q?=E5=8F=8C=E8=BD=B4=20+=20a11y=20=E4=B8=8E=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend - Post 加 cover_url,Tag 加 color;老库 ALTER 兼容 - admin API: GET /api/admin/dashboard,POST /bulk,POST /tags/:id/merge, PUT /tags/:id 接 {name,color},Settings 加 light_skin_id(兼容老 theme_id) - 白名单兜底(非法 light_skin_id 落 paper) - 新增合并 / Dashboard / Theme 测试覆盖 Frontend - AdminLayout 改为侧边栏布局;新增 Dashboard 总览页 - PostsView 卡片化预览 + 筛选 chip + 批量操作 + 排序 - EditorView 增强:封面图 / WYSIWYG↔MD 双模式 / 实时大纲 / 字数统计 / 自动保存条 / ⌘S / unsaved 守卫(beforeunload + beforeRouteLeave) - TagsView 加色板 + 合并到目标 - SettingsView 主题外观改为「亮色皮肤」三选一 + 「暗色皮肤」固定墨色说明 - 新增 CommandPalette(⌘K)+ ShortcutsPanel(?) - api.js 接新接口;router.js 拆 /admin → /admin/posts 主题 / 皮肤双轴 - site.js: themeMode(light/dark/auto)+ light_skin_id;data-theme 前台, data-admin-theme 后台(仅跟随 mode 切明暗) - styles.css: :root[data-theme] 四套皮肤 + :root[data-admin-theme=dark] 后台墨感 - 修 sticky head / ThemeSwitcher / 表单 / 批量条 等 token 引用 - 后台用独立 --admin-* 系列,与前台主题解耦 - 左栏底部放 inline 形态 ThemeSwitcher(图标按钮 36×36),mobile 保留浮窗 a11y - 全局 :focus-visible 描边(前后台跟随 token) - prefers-reduced-motion 全局降级 - color-scheme 随主题切 - meta theme-color 双套(light/dark) - 命令面板 / 快捷键面板:role=dialog aria-modal aria-label + 搜索框 aria-label - icon-only 按钮加 aria-label(tag × / 清除选择 / 移除封面等) - 输入框 aria-label + spellcheck=false - PostsView 筛选同步 URL(可分享、可刷新保留) - img width/height + loading=lazy - utils.js 用 Intl.DateTimeFormat(zh-CN / sv-SE) --- backend/internal/admin/api.go | 120 +- backend/internal/model/model.go | 32 + backend/internal/store/store.go | 353 +++- backend/internal/store/store_test.go | 224 +++ frontend/index.html | 2 + frontend/package.json | 5 +- frontend/pnpm-lock.yaml | 1840 +++++++++++++++++++++ frontend/src/App.vue | 35 +- frontend/src/admin/AdminLayout.vue | 213 +-- frontend/src/admin/CommandPalette.vue | 133 ++ frontend/src/admin/DashboardView.vue | 140 ++ frontend/src/admin/EditorView.vue | 660 ++++---- frontend/src/admin/LoginView.vue | 14 +- frontend/src/admin/PostsView.vue | 418 +++-- frontend/src/admin/SettingsView.vue | 224 +-- frontend/src/admin/ShortcutsPanel.vue | 67 + frontend/src/admin/TagsView.vue | 224 ++- frontend/src/api.js | 9 +- frontend/src/components/LeftNav.vue | 32 +- frontend/src/components/PostCard.vue | 2 +- frontend/src/components/ThemeSwitcher.vue | 103 ++ frontend/src/components/TopBar.vue | 2 +- frontend/src/router.js | 3 +- frontend/src/site.js | 102 +- frontend/src/styles.css | 1798 +++++++++++++++++++- frontend/src/utils.js | 17 +- frontend/src/views/AboutView.vue | 2 +- frontend/src/views/ArchiveView.vue | 2 +- frontend/src/views/HomeView.vue | 15 +- frontend/src/views/TagView.vue | 2 +- frontend/src/views/TagsView.vue | 2 +- frontend/tsconfig.json | 4 +- 32 files changed, 5823 insertions(+), 976 deletions(-) create mode 100644 frontend/src/admin/CommandPalette.vue create mode 100644 frontend/src/admin/DashboardView.vue create mode 100644 frontend/src/admin/ShortcutsPanel.vue create mode 100644 frontend/src/components/ThemeSwitcher.vue diff --git a/backend/internal/admin/api.go b/backend/internal/admin/api.go index f4b5beb..1d99990 100644 --- a/backend/internal/admin/api.go +++ b/backend/internal/admin/api.go @@ -29,6 +29,7 @@ func (a *API) Routes() http.Handler { mux.HandleFunc("/api/admin/logout", a.logout) mux.HandleFunc("/api/admin/me", a.guard(a.me)) + mux.HandleFunc("/api/admin/dashboard", a.guard(a.dashboard)) mux.HandleFunc("/api/admin/posts", a.guard(a.listPosts)) mux.HandleFunc("/api/admin/posts/", a.guard(a.postByID)) mux.HandleFunc("/api/admin/tags", a.guard(a.listTags)) @@ -128,12 +129,13 @@ func (a *API) listPosts(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: o := store.ListOptions{ - Kind: httpx.QueryString(r, "kind"), - Tag: httpx.QueryString(r, "tag"), - Query: httpx.QueryString(r, "q"), - Status: httpx.QueryString(r, "status"), - Page: httpx.QueryInt(r, "page", 1), - Size: httpx.QueryInt(r, "size", 20), + Kind: httpx.QueryString(r, "kind"), + Tag: httpx.QueryString(r, "tag"), + Query: httpx.QueryString(r, "q"), + Status: httpx.QueryString(r, "status"), + OrderBy: httpx.QueryString(r, "order"), + Page: httpx.QueryInt(r, "page", 1), + Size: httpx.QueryInt(r, "size", 20), } if o.Status == "" { o.Status = "any" @@ -150,7 +152,7 @@ func (a *API) listPosts(w http.ResponseWriter, r *http.Request) { httpx.BadRequest(w, "invalid body") return } - in.Status = normalizeStatus(in.Status) + in.Status = store.NormalizeStatus(in.Status) p, err := a.Store.Create(in) if err != nil { httpx.ServerError(w, err) @@ -162,21 +164,16 @@ func (a *API) listPosts(w http.ResponseWriter, r *http.Request) { } } -func normalizeStatus(s string) string { - switch strings.ToLower(strings.TrimSpace(s)) { - case model.StatusDraft, model.StatusPublished: - return strings.ToLower(strings.TrimSpace(s)) - default: - return model.StatusDraft - } -} - func (a *API) postByID(w http.ResponseWriter, r *http.Request) { rest := strings.Trim(strings.TrimPrefix(r.URL.Path, "/api/admin/posts/"), "/") if rest == "" { a.listPosts(w, r) return } + if rest == "bulk" { + a.bulkPosts(w, r) + return + } id, err := parseInt(rest) if err != nil { httpx.BadRequest(w, "bad post id") @@ -193,7 +190,7 @@ func (a *API) postByID(w http.ResponseWriter, r *http.Request) { return } if in.Status != "" { - in.Status = normalizeStatus(in.Status) + in.Status = store.NormalizeStatus(in.Status) } p, err := a.Store.Update(id, in) writeOne(w, p, err) @@ -264,21 +261,27 @@ func (a *API) listTags(w http.ResponseWriter, r *http.Request) { func (a *API) tagByID(w http.ResponseWriter, r *http.Request) { rest := strings.Trim(strings.TrimPrefix(r.URL.Path, "/api/admin/tags/"), "/") - id, err := parseInt(rest) + parts := strings.SplitN(rest, "/", 2) + id, err := parseInt(parts[0]) if err != nil { httpx.BadRequest(w, "bad tag id") return } + if len(parts) == 2 && parts[1] == "merge" { + a.mergeTag(w, r, id) + return + } switch r.Method { case http.MethodPut, http.MethodPatch: var in struct { - Name string `json:"name"` + Name string `json:"name"` + Color string `json:"color"` } if err := httpx.Decode(r, &in); err != nil { httpx.BadRequest(w, "invalid body") return } - t, err := a.Store.RenameTag(id, in.Name) + t, err := a.Store.UpdateTag(id, in.Name, in.Color) writeTag(w, t, err) case http.MethodDelete: if err := a.Store.DeleteTag(id); err != nil { @@ -291,6 +294,26 @@ func (a *API) tagByID(w http.ResponseWriter, r *http.Request) { } } +func (a *API) mergeTag(w http.ResponseWriter, r *http.Request, fromID int64) { + if r.Method != http.MethodPost { + httpx.Error(w, http.StatusMethodNotAllowed, "POST required") + return + } + var in struct { + ToID int64 `json:"to_id"` + } + if err := httpx.Decode(r, &in); err != nil { + httpx.BadRequest(w, "invalid body") + return + } + if in.ToID == 0 { + httpx.BadRequest(w, "to_id required") + return + } + t, err := a.Store.MergeTags(fromID, in.ToID) + writeTag(w, t, err) +} + func writeTag(w http.ResponseWriter, t model.Tag, err error) { if err != nil { if errors.Is(err, store.ErrNotFound) { @@ -348,3 +371,60 @@ func parseInt(s string) (int64, error) { } return n, nil } + +// ---------- dashboard ---------- + +func (a *API) dashboard(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + httpx.Error(w, http.StatusMethodNotAllowed, "GET required") + return + } + d, err := a.Store.Dashboard() + if err != nil { + httpx.ServerError(w, err) + return + } + httpx.OK(w, d) +} + +// ---------- bulk posts ---------- + +type bulkPostsRequest struct { + IDs []int64 `json:"ids"` + Action string `json:"action"` // "publish" | "draft" | "delete" +} + +func (a *API) bulkPosts(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + httpx.Error(w, http.StatusMethodNotAllowed, "POST required") + return + } + var in bulkPostsRequest + if err := httpx.Decode(r, &in); err != nil { + httpx.BadRequest(w, "invalid body") + return + } + if len(in.IDs) == 0 { + httpx.BadRequest(w, "ids required") + return + } + switch in.Action { + case "publish", "draft": + n, err := a.Store.BulkUpdateStatus(in.IDs, store.NormalizeStatus(in.Action)) + if err != nil { + httpx.ServerError(w, err) + return + } + httpx.OK(w, map[string]any{"ok": true, "updated": n}) + case "delete": + var failed int + for _, id := range in.IDs { + if err := a.Store.Delete(id); err != nil { + failed++ + } + } + httpx.OK(w, map[string]any{"ok": true, "deleted": len(in.IDs) - failed, "failed": failed}) + default: + httpx.BadRequest(w, "action must be one of publish|draft|delete") + } +} diff --git a/backend/internal/model/model.go b/backend/internal/model/model.go index 547d49e..e5309aa 100644 --- a/backend/internal/model/model.go +++ b/backend/internal/model/model.go @@ -18,6 +18,7 @@ type Post struct { Title string `json:"title"` Slug string `json:"slug"` Summary string `json:"summary"` + CoverURL string `json:"cover_url"` ContentMd string `json:"content_md,omitempty"` ContentHTML string `json:"content_html"` Status string `json:"status"` @@ -33,6 +34,7 @@ type PostInput struct { Title string `json:"title"` Slug string `json:"slug"` Summary string `json:"summary"` + CoverURL string `json:"cover_url"` ContentMd string `json:"content_md"` Status string `json:"status"` PublishedAt string `json:"published_at"` @@ -44,6 +46,7 @@ type Tag struct { ID int64 `json:"id"` Name string `json:"name"` Slug string `json:"slug"` + Color string `json:"color"` Count int `json:"count"` } @@ -65,6 +68,26 @@ type Page struct { Size int `json:"size"` } +// Dashboard is the snapshot rendered on /admin (homepage). +type Dashboard struct { + TotalPosts int `json:"total_posts"` + PublishedPosts int `json:"published_posts"` + DraftPosts int `json:"draft_posts"` + ShortPosts int `json:"short_posts"` + LongPosts int `json:"long_posts"` + TotalTags int `json:"total_tags"` + TotalWords int `json:"total_words"` + RecentPosts []Post `json:"recent_posts"` + RecentDrafts []Post `json:"recent_drafts"` + TopTags []Tag `json:"top_tags"` + PublishedByMonth []MonthBucket `json:"published_by_month"` +} + +type MonthBucket struct { + Month string `json:"month"` // "YYYY-MM" + Count int `json:"count"` +} + type Settings struct { SiteTitle string `json:"site_title"` SiteDesc string `json:"site_desc"` @@ -73,4 +96,13 @@ type Settings struct { FooterNote string `json:"footer_note"` ICPLicense string `json:"icp"` PostsPerPage int `json:"posts_per_page"` + // LightSkinID is the front-end skin used when the client (or system) + // prefers light. Valid values: paper / sage / rose. + // Dark side is fixed to ink for now — kept implicit so we can add + // dark variants later without breaking clients. + LightSkinID string `json:"light_skin_id"` + // ThemeID is kept for backward compatibility with older clients that + // only know about a single skin. settingsFromMap falls back to it + // when LightSkinID is empty. + ThemeID string `json:"theme_id,omitempty"` } diff --git a/backend/internal/store/store.go b/backend/internal/store/store.go index 0efa27f..b852338 100644 --- a/backend/internal/store/store.go +++ b/backend/internal/store/store.go @@ -44,6 +44,7 @@ func (s *Store) migrate() error { title TEXT NOT NULL DEFAULT '', slug TEXT NOT NULL, summary TEXT NOT NULL DEFAULT '', + cover_url TEXT NOT NULL DEFAULT '', content_md TEXT NOT NULL DEFAULT '', content_html TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'draft', @@ -53,9 +54,10 @@ func (s *Store) migrate() error { reading_minutes INTEGER NOT NULL DEFAULT 1 )`, ai), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS tags ( - id %s, - name TEXT NOT NULL, - slug TEXT NOT NULL + id %s, + name TEXT NOT NULL, + slug TEXT NOT NULL, + color TEXT NOT NULL DEFAULT '' )`, ai), `CREATE TABLE IF NOT EXISTS post_tags ( post_id INTEGER NOT NULL, @@ -73,6 +75,20 @@ func (s *Store) migrate() error { } } + // Lightweight column-add migrations for older databases. SQLite supports + // ADD COLUMN; Postgres doesn't support IF NOT EXISTS on column add, so we + // swallow "already exists" errors either way. + columnAdds := []string{ + `ALTER TABLE posts ADD COLUMN cover_url TEXT NOT NULL DEFAULT ''`, + `ALTER TABLE tags ADD COLUMN color TEXT NOT NULL DEFAULT ''`, + } + for _, q := range columnAdds { + if _, err := s.db.Exec(s.db.Q(q)); err != nil && !strings.Contains(err.Error(), "already exists") && + !strings.Contains(err.Error(), "duplicate column") { + return fmt.Errorf("migrate column: %w", err) + } + } + // Indexes/unique constraints need dialect-specific "IF NOT EXISTS" support. indexes := []struct{ name, ddl string }{ {"idx_posts_slug", `CREATE UNIQUE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug)`}, @@ -144,6 +160,18 @@ func settingsFromMap(m map[string]string) model.Settings { ICPLicense: m["icp"], PostsPerPage: 10, } + // LightSkinID is the new canonical name; fall back to legacy theme_id + // for clients that haven't been updated, and finally to "paper". + st.LightSkinID = m["light_skin_id"] + if st.LightSkinID == "" { + st.LightSkinID = m["theme_id"] + } + if st.LightSkinID == "" { + st.LightSkinID = "paper" + } + // Mirror the value into ThemeID so legacy API consumers still see it + // in the JSON response. + st.ThemeID = st.LightSkinID if n := atoi(m["posts_per_page"]); n > 0 { st.PostsPerPage = n } @@ -161,10 +189,21 @@ func atoi(v string) int { return n } +// ValidLightSkins is the whitelist of light-side skin ids the API accepts. +// Anything outside this set falls back to "paper" on write. +var ValidLightSkins = map[string]bool{ + "paper": true, + "sage": true, + "rose": true, +} + func (s *Store) UpdateSettings(st model.Settings) error { if st.PostsPerPage <= 0 { st.PostsPerPage = 10 } + if !ValidLightSkins[st.LightSkinID] { + st.LightSkinID = "paper" + } sets := map[string]string{ "site_title": st.SiteTitle, "site_desc": st.SiteDesc, @@ -173,6 +212,9 @@ func (s *Store) UpdateSettings(st model.Settings) error { "footer_note": st.FooterNote, "icp": st.ICPLicense, "posts_per_page": fmt.Sprint(st.PostsPerPage), + "light_skin_id": st.LightSkinID, + // Mirror to theme_id so any older client still sees something. + "theme_id": st.LightSkinID, } for k, v := range sets { if s.db.Dialect == db.Postgres { @@ -202,13 +244,13 @@ type ListOptions struct { OrderBy string } -const postCols = `id, kind, title, slug, summary, content_md, content_html, status, +const postCols = `id, kind, title, slug, summary, cover_url, content_md, content_html, status, published_at, created_at, updated_at, reading_minutes` func scanPost(rows interface{ Scan(...any) error }) (model.Post, error) { var p model.Post - err := rows.Scan(&p.ID, &p.Kind, &p.Title, &p.Slug, &p.Summary, &p.ContentMd, - &p.ContentHTML, &p.Status, &p.PublishedAt, &p.CreatedAt, &p.UpdatedAt, &p.ReadingMinutes) + err := rows.Scan(&p.ID, &p.Kind, &p.Title, &p.Slug, &p.Summary, &p.CoverURL, + &p.ContentMd, &p.ContentHTML, &p.Status, &p.PublishedAt, &p.CreatedAt, &p.UpdatedAt, &p.ReadingMinutes) p.Tags = []string{} return p, err } @@ -340,8 +382,8 @@ func (s *Store) GetBySlug(slug string) (model.Post, error) { } func scanPostInto(row *sql.Row, p *model.Post) error { - err := row.Scan(&p.ID, &p.Kind, &p.Title, &p.Slug, &p.Summary, &p.ContentMd, - &p.ContentHTML, &p.Status, &p.PublishedAt, &p.CreatedAt, &p.UpdatedAt, &p.ReadingMinutes) + err := row.Scan(&p.ID, &p.Kind, &p.Title, &p.Slug, &p.Summary, &p.CoverURL, + &p.ContentMd, &p.ContentHTML, &p.Status, &p.PublishedAt, &p.CreatedAt, &p.UpdatedAt, &p.ReadingMinutes) if err == sql.ErrNoRows { return ErrNotFound } @@ -386,11 +428,12 @@ func (s *Store) uniqueSlug(base string, excludeID int64) string { func (s *Store) Create(in model.PostInput) (model.Post, error) { p := model.Post{ - Kind: in.Kind, - Title: strings.TrimSpace(in.Title), - Slug: in.Slug, - Status: in.Status, - Tags: []string{}, + Kind: in.Kind, + Title: strings.TrimSpace(in.Title), + Slug: in.Slug, + Status: in.Status, + CoverURL: strings.TrimSpace(in.CoverURL), + Tags: []string{}, } if p.Kind == "" { p.Kind = model.KindLong @@ -428,17 +471,17 @@ func (s *Store) Create(in model.PostInput) (model.Post, error) { } var id int64 - q := s.db.Q(`INSERT INTO posts (kind,title,slug,summary,content_md,content_html,status, + q := s.db.Q(`INSERT INTO posts (kind,title,slug,summary,cover_url,content_md,content_html,status, published_at,created_at,updated_at,reading_minutes) - VALUES (?,?,?,?,?,?,?,?,?,?,?)`) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`) if s.db.Dialect == db.Postgres { - err := s.db.QueryRow(q, p.Kind, p.Title, p.Slug, p.Summary, p.ContentMd, p.ContentHTML, + err := s.db.QueryRow(q, p.Kind, p.Title, p.Slug, p.Summary, p.CoverURL, p.ContentMd, p.ContentHTML, p.Status, p.PublishedAt, p.CreatedAt, p.UpdatedAt, p.ReadingMinutes).Scan(&id) if err != nil { return p, err } } else { - res, err := s.db.Exec(q, p.Kind, p.Title, p.Slug, p.Summary, p.ContentMd, p.ContentHTML, + res, err := s.db.Exec(q, p.Kind, p.Title, p.Slug, p.Summary, p.CoverURL, p.ContentMd, p.ContentHTML, p.Status, p.PublishedAt, p.CreatedAt, p.UpdatedAt, p.ReadingMinutes) if err != nil { return p, err @@ -483,6 +526,10 @@ func (s *Store) Update(id int64, in model.PostInput) (model.Post, error) { if in.PublishedAt != "" { p.PublishedAt = in.PublishedAt } + // CoverURL is taken verbatim from input. The frontend always sends the + // current value (empty string included), so any update round-trips with + // whatever the user last saved. + p.CoverURL = strings.TrimSpace(in.CoverURL) p.UpdatedAt = now() if in.ReadingMinutes != nil && *in.ReadingMinutes > 0 { p.ReadingMinutes = *in.ReadingMinutes @@ -490,9 +537,9 @@ func (s *Store) Update(id int64, in model.PostInput) (model.Post, error) { p.ReadingMinutes = readingMinutes(in.ContentMd) } - if _, err := s.db.Exec(s.db.Q(`UPDATE posts SET kind=?,title=?,slug=?,summary=?,content_md=?, + if _, err := s.db.Exec(s.db.Q(`UPDATE posts SET kind=?,title=?,slug=?,summary=?,cover_url=?,content_md=?, content_html=?,status=?,published_at=?,updated_at=?,reading_minutes=? WHERE id=?`), - p.Kind, p.Title, p.Slug, p.Summary, p.ContentMd, p.ContentHTML, p.Status, + p.Kind, p.Title, p.Slug, p.Summary, p.CoverURL, p.ContentMd, p.ContentHTML, p.Status, p.PublishedAt, p.UpdatedAt, p.ReadingMinutes, id); err != nil { return p, err } @@ -504,6 +551,37 @@ func (s *Store) Update(id int64, in model.PostInput) (model.Post, error) { return s.Get(id) } +// BulkUpdateStatus flips the status of many posts in a single transaction. +func (s *Store) BulkUpdateStatus(ids []int64, status string) (int, error) { + if len(ids) == 0 { + return 0, nil + } + status = NormalizeStatus(status) + placeholders := strings.TrimSuffix(strings.Repeat("?,", len(ids)), ",") + args := []any{status, now()} + for _, id := range ids { + args = append(args, id) + } + q := s.db.Q(`UPDATE posts SET status=?, updated_at=? WHERE id IN (` + placeholders + `)`) + res, err := s.db.Exec(q, args...) + if err != nil { + return 0, err + } + n, _ := res.RowsAffected() + return int(n), nil +} + +// NormalizeStatus coerces an arbitrary input string into a known post status +// value, defaulting to draft when nothing matches. +func NormalizeStatus(s string) string { + switch strings.ToLower(strings.TrimSpace(s)) { + case model.StatusDraft, model.StatusPublished: + return strings.ToLower(strings.TrimSpace(s)) + default: + return model.StatusDraft + } +} + func (s *Store) Delete(id int64) error { if _, err := s.db.Exec(s.db.Q(`DELETE FROM post_tags WHERE post_id = ?`), id); err != nil { return err @@ -568,9 +646,9 @@ func (s *Store) upsertTag(name string) (int64, error) { } func (s *Store) ListTags() ([]model.Tag, error) { - q := s.db.Q(`SELECT t.id, t.name, t.slug, COUNT(pt.post_id) AS c + q := s.db.Q(`SELECT t.id, t.name, t.slug, t.color, COUNT(pt.post_id) AS c FROM tags t LEFT JOIN post_tags pt ON pt.tag_id = t.id - GROUP BY t.id, t.name, t.slug ORDER BY c DESC, t.name`) + GROUP BY t.id, t.name, t.slug, t.color ORDER BY c DESC, t.name`) rows, err := s.db.Query(q) if err != nil { return nil, err @@ -579,7 +657,7 @@ func (s *Store) ListTags() ([]model.Tag, error) { out := []model.Tag{} for rows.Next() { var t model.Tag - if err := rows.Scan(&t.ID, &t.Name, &t.Slug, &t.Count); err != nil { + if err := rows.Scan(&t.ID, &t.Name, &t.Slug, &t.Color, &t.Count); err != nil { return nil, err } out = append(out, t) @@ -599,6 +677,59 @@ func (s *Store) CreateTag(name string) (model.Tag, error) { return model.Tag{ID: id, Name: name, Slug: Slugify(name)}, nil } +// CreateTagFull is used by the admin API when color is supplied. +func (s *Store) CreateTagFull(name, color string) (model.Tag, error) { + name = strings.TrimSpace(name) + if name == "" { + return model.Tag{}, errors.New("tag name required") + } + slug := Slugify(name) + if slug == "" { + return model.Tag{}, errors.New("tag slug required") + } + var id int64 + err := s.db.QueryRow(s.db.Q(`SELECT id FROM tags WHERE slug = ?`), slug).Scan(&id) + if err != nil && err != sql.ErrNoRows { + return model.Tag{}, err + } + if err == nil { + // already exists — update color and return + if _, err := s.db.Exec(s.db.Q(`UPDATE tags SET color=? WHERE id=?`), color, id); err != nil { + return model.Tag{}, err + } + return s.getTag(id) + } + color = strings.TrimSpace(color) + if s.db.Dialect == db.Postgres { + err := s.db.QueryRow(s.db.Q(`INSERT INTO tags(name,slug,color) VALUES (?,?,?) RETURNING id`), + name, slug, color).Scan(&id) + if err != nil { + return model.Tag{}, err + } + } else { + res, err := s.db.Exec(s.db.Q(`INSERT INTO tags(name,slug,color) VALUES (?,?,?)`), name, slug, color) + if err != nil { + return model.Tag{}, err + } + id, err = res.LastInsertId() + if err != nil { + return model.Tag{}, err + } + } + return s.getTag(id) +} + +func (s *Store) getTag(id int64) (model.Tag, error) { + var t model.Tag + err := s.db.QueryRow(s.db.Q(`SELECT t.id, t.name, t.slug, t.color, COUNT(pt.post_id) + FROM tags t LEFT JOIN post_tags pt ON pt.tag_id = t.id + WHERE t.id = ? GROUP BY t.id`), id).Scan(&t.ID, &t.Name, &t.Slug, &t.Color, &t.Count) + if err == sql.ErrNoRows { + return t, ErrNotFound + } + return t, err +} + func (s *Store) RenameTag(id int64, name string) (model.Tag, error) { name = strings.TrimSpace(name) slug := Slugify(name) @@ -608,13 +739,49 @@ func (s *Store) RenameTag(id int64, name string) (model.Tag, error) { if _, err := s.db.Exec(s.db.Q(`UPDATE tags SET name=?, slug=? WHERE id=?`), name, slug, id); err != nil { return model.Tag{}, err } - var t model.Tag - err := s.db.QueryRow(s.db.Q(`SELECT id, name, slug, 0 FROM tags WHERE id = ?`), id). - Scan(&t.ID, &t.Name, &t.Slug, &t.Count) - if err == sql.ErrNoRows { - return t, ErrNotFound + return s.getTag(id) +} + +// UpdateTag is the combined rename + recolor used by the admin UI. +func (s *Store) UpdateTag(id int64, name, color string) (model.Tag, error) { + name = strings.TrimSpace(name) + slug := Slugify(name) + if name == "" { + return model.Tag{}, errors.New("tag name required") } - return t, err + color = strings.TrimSpace(color) + if _, err := s.db.Exec(s.db.Q(`UPDATE tags SET name=?, slug=?, color=? WHERE id=?`), name, slug, color, id); err != nil { + return model.Tag{}, err + } + return s.getTag(id) +} + +// MergeTags moves every post from fromID to toID, then deletes fromID. It is a +// no-op if the ids are equal or either doesn't exist. +func (s *Store) MergeTags(fromID, toID int64) (model.Tag, error) { + if fromID == toID { + return model.Tag{}, errors.New("source and target are the same tag") + } + if _, err := s.getTag(fromID); err != nil { + return model.Tag{}, err + } + if _, err := s.getTag(toID); err != nil { + return model.Tag{}, err + } + // De-duplicate before the move so we don't end up with two rows pointing + // at the same post. + if _, err := s.db.Exec(s.db.Q(`DELETE FROM post_tags + WHERE post_id IN (SELECT post_id FROM post_tags WHERE tag_id = ?) + AND tag_id = ?`), toID, toID); err != nil { + return model.Tag{}, err + } + if _, err := s.db.Exec(s.db.Q(`UPDATE post_tags SET tag_id=? WHERE tag_id=?`), toID, fromID); err != nil { + return model.Tag{}, err + } + if _, err := s.db.Exec(s.db.Q(`DELETE FROM tags WHERE id=?`), fromID); err != nil { + return model.Tag{}, err + } + return s.getTag(toID) } func (s *Store) DeleteTag(id int64) error { @@ -667,3 +834,133 @@ func splitDate(rfc3339 string) (string, string) { } return rfc3339[0:4], rfc3339[5:7] } + +// ---------- dashboard ---------- + +// Dashboard produces the snapshot rendered on the admin home page. +func (s *Store) Dashboard() (model.Dashboard, error) { + d := model.Dashboard{ + RecentPosts: []model.Post{}, + RecentDrafts: []model.Post{}, + TopTags: []model.Tag{}, + PublishedByMonth: []model.MonthBucket{}, + } + + // status / kind counts -------------------------------------------------- + rows, err := s.db.Query(s.db.Q(`SELECT status, kind, COUNT(*) FROM posts GROUP BY status, kind`)) + if err != nil { + return d, err + } + for rows.Next() { + var status, kind string + var n int + if err := rows.Scan(&status, &kind, &n); err != nil { + rows.Close() + return d, err + } + d.TotalPosts += n + switch status { + case model.StatusPublished: + d.PublishedPosts += n + case model.StatusDraft: + d.DraftPosts += n + } + switch kind { + case model.KindShort: + d.ShortPosts += n + case model.KindLong: + d.LongPosts += n + } + } + rows.Close() + + // total tag count ------------------------------------------------------- + if err := s.db.QueryRow(s.db.Q(`SELECT COUNT(*) FROM tags`)).Scan(&d.TotalTags); err != nil { + return d, err + } + + // total word count: sum of content_md length ---------------------------- + if err := s.db.QueryRow(s.db.Q(`SELECT COALESCE(SUM(LENGTH(content_md)),0) FROM posts`)).Scan(&d.TotalWords); err != nil { + return d, err + } + + // recent published posts (5) -------------------------------------------- + if err := s.scanPostsInto(s.db.Q(`SELECT `+postCols+` FROM posts + WHERE status=? ORDER BY published_at DESC LIMIT 5`), []any{model.StatusPublished}, &d.RecentPosts); err != nil { + return d, err + } + + // recent drafts (5) ----------------------------------------------------- + if err := s.scanPostsInto(s.db.Q(`SELECT `+postCols+` FROM posts + WHERE status=? ORDER BY updated_at DESC LIMIT 5`), []any{model.StatusDraft}, &d.RecentDrafts); err != nil { + return d, err + } + + // top tags (10) --------------------------------------------------------- + if err := s.db.QueryRow(s.db.Q(`SELECT COUNT(*) FROM tags`)).Scan(&d.TotalTags); err != nil { + return d, err + } + tagRows, err := s.db.Query(s.db.Q(`SELECT t.id, t.name, t.slug, t.color, COUNT(pt.post_id) AS c + FROM tags t LEFT JOIN post_tags pt ON pt.tag_id = t.id + GROUP BY t.id, t.name, t.slug, t.color ORDER BY c DESC, t.name LIMIT 10`)) + if err != nil { + return d, err + } + for tagRows.Next() { + var t model.Tag + if err := tagRows.Scan(&t.ID, &t.Name, &t.Slug, &t.Color, &t.Count); err != nil { + tagRows.Close() + return d, err + } + d.TopTags = append(d.TopTags, t) + } + tagRows.Close() + + // published_by_month — last 12 months ----------------------------------- + monthRows, err := s.db.Query(s.db.Q(`SELECT substr(published_at,1,7) AS m, COUNT(*) AS c + FROM posts WHERE status=? AND length(published_at) >= 7 + GROUP BY m ORDER BY m DESC LIMIT 12`), model.StatusPublished) + if err != nil { + return d, err + } + defer monthRows.Close() + for monthRows.Next() { + var b model.MonthBucket + if err := monthRows.Scan(&b.Month, &b.Count); err != nil { + return d, err + } + d.PublishedByMonth = append(d.PublishedByMonth, b) + } + // reverse so we can render left-to-right on the chart + for i, j := 0, len(d.PublishedByMonth)-1; i < j; i, j = i+1, j-1 { + d.PublishedByMonth[i], d.PublishedByMonth[j] = d.PublishedByMonth[j], d.PublishedByMonth[i] + } + + return d, nil +} + +// scanPostsInto is a small helper that runs a query and scans rows into the +// given slice, attaching tags at the end. +func (s *Store) scanPostsInto(query string, args []any, dst *[]model.Post) error { + rows, err := s.db.Query(query, args...) + if err != nil { + return err + } + defer rows.Close() + out := []model.Post{} + for rows.Next() { + p, err := scanPost(rows) + if err != nil { + return err + } + out = append(out, p) + } + if err := rows.Err(); err != nil { + return err + } + if err := s.attachTags(out); err != nil { + return err + } + *dst = out + return nil +} diff --git a/backend/internal/store/store_test.go b/backend/internal/store/store_test.go index 026fcaf..c317e0b 100644 --- a/backend/internal/store/store_test.go +++ b/backend/internal/store/store_test.go @@ -40,3 +40,227 @@ func TestPostInputDefaults(t *testing.T) { t.Errorf("empty status means draft is applied by the API layer, got %q", in.Status) } } + +func TestNormalizeStatus(t *testing.T) { + cases := map[string]string{ + "": model.StatusDraft, + "draft": model.StatusDraft, + "published": model.StatusPublished, + " Published ": model.StatusPublished, + "pending": model.StatusDraft, // unknown → draft + } + for in, want := range cases { + if got := NormalizeStatus(in); got != want { + t.Errorf("NormalizeStatus(%q) = %q, want %q", in, got, want) + } + } +} + +func TestRebindBulk(t *testing.T) { + // BulkUpdateStatus uses an IN clause with len(ids) placeholders + 1 for status. + d := &db.DB{Dialect: db.Postgres} + q := d.Q(`UPDATE posts SET status=? WHERE id IN (?,?,?)`) + if got := strings.Count(q, "$"); got != 4 { + t.Errorf("expected 4 placeholders, got %d", got) + } +} + +func TestSettingsThemeID(t *testing.T) { + s := openTestStore(t) + + st, err := s.GetSettings() + if err != nil { + t.Fatalf("get: %v", err) + } + if st.LightSkinID != "paper" { + t.Errorf("default light_skin_id should be 'paper', got %q", st.LightSkinID) + } + + st.LightSkinID = "sage" + st.SiteTitle = "ONE" + if err := s.UpdateSettings(st); err != nil { + t.Fatalf("update: %v", err) + } + got, err := s.GetSettings() + if err != nil { + t.Fatalf("get again: %v", err) + } + if got.LightSkinID != "sage" { + t.Errorf("light_skin_id should persist, got %q", got.LightSkinID) + } + if got.ThemeID != "sage" { + t.Errorf("legacy theme_id should mirror light_skin_id, got %q", got.ThemeID) + } + if got.SiteTitle != "ONE" { + t.Errorf("site_title should persist, got %q", got.SiteTitle) + } +} + +func TestSettingsLightSkinFallback(t *testing.T) { + s := openTestStore(t) + // Simulate an older DB that only has theme_id set. + if _, err := s.db.Exec(s.db.Q(`INSERT INTO settings(key,value) VALUES (?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`), + "theme_id", "rose"); err != nil { + t.Fatalf("seed theme_id: %v", err) + } + st, err := s.GetSettings() + if err != nil { + t.Fatalf("get: %v", err) + } + if st.LightSkinID != "rose" { + t.Errorf("expected fallback to 'rose' from legacy theme_id, got %q", st.LightSkinID) + } +} + +// TestStoreSQLite runs a small integration pass on top of an in-memory SQLite +// database to exercise the new fields (cover_url, tag color) and the merge +// + bulk + dashboard paths. +func TestStoreSQLite(t *testing.T) { + s := openTestStore(t) + + p1, err := s.Create(model.PostInput{ + Kind: model.KindLong, + Title: "第一篇", + Slug: "first", + Summary: "first summary", + CoverURL: "https://example.com/a.png", + ContentMd: "# hi\n这是一段正文。", + Status: model.StatusPublished, + Tags: []string{"Go", "博客"}, + }) + if err != nil { + t.Fatalf("create p1: %v", err) + } + if p1.CoverURL == "" { + t.Errorf("cover_url not persisted") + } + if len(p1.Tags) != 2 { + t.Errorf("expected 2 tags, got %v", p1.Tags) + } + + p2, err := s.Create(model.PostInput{ + Kind: model.KindShort, ContentMd: "碎片想法。", Status: model.StatusDraft, + Tags: []string{"碎片"}, + }) + if err != nil { + t.Fatalf("create p2: %v", err) + } + + // Update cover_url and tags. Frontend sends the current CoverURL back so + // the value is preserved across updates. + upd, err := s.Update(p1.ID, model.PostInput{ + Kind: model.KindLong, Title: "第一篇", + CoverURL: p1.CoverURL, + ContentMd: "# hi\n这是新版本。", + Tags: []string{"Go"}, + }) + if err != nil { + t.Fatalf("update: %v", err) + } + if upd.CoverURL != p1.CoverURL { + t.Errorf("cover_url not preserved when echoed back, got %q", upd.CoverURL) + } + + // An empty CoverURL is treated as an explicit clear. + upd2, err := s.Update(p1.ID, model.PostInput{ + Kind: model.KindLong, Title: "第一篇", CoverURL: "", + Tags: []string{"Go"}, + }) + if err != nil { + t.Fatalf("update clear: %v", err) + } + if upd2.CoverURL != "" { + t.Errorf("empty cover_url should clear, got %q", upd2.CoverURL) + } + // restore for downstream tests + if _, err := s.Update(p1.ID, model.PostInput{ + Kind: model.KindLong, Title: "第一篇", CoverURL: p1.CoverURL, + Tags: []string{"Go"}, + }); err != nil { + t.Fatalf("restore: %v", err) + } + + if len(upd.Tags) != 1 || upd.Tags[0] != "Go" { + t.Errorf("tags after update: %v", upd.Tags) + } + + // BulkUpdateStatus flips drafts to published + n, err := s.BulkUpdateStatus([]int64{p2.ID}, model.StatusPublished) + if err != nil { + t.Fatalf("bulk: %v", err) + } + if n != 1 { + t.Errorf("expected 1 updated, got %d", n) + } + + // Tag merge: create two distinct slug tags, attach both to p1, merge. + if _, err := s.UpdateTag(0, "", ""); err == nil { + t.Errorf("expected error for empty name") + } + tags, err := s.ListTags() + if err != nil { + t.Fatalf("list tags: %v", err) + } + var goID int64 + for _, tg := range tags { + if tg.Name == "Go" { + goID = tg.ID + } + } + if goID == 0 { + t.Fatalf("Go tag not found") + } + // Give the surviving target tag a color so we can confirm it sticks. + if _, err := s.UpdateTag(goID, "Go", "#3d7f9c"); err != nil { + t.Fatalf("color target: %v", err) + } + // CreateTagFull uses upsertTag so "GoLang" with its own slug is a fresh row. + golang, err := s.CreateTagFull("GoLang", "") + if err != nil { + t.Fatalf("create golang: %v", err) + } + if _, err := s.db.Exec(s.db.Q(`INSERT INTO post_tags(post_id, tag_id) VALUES (?, ?)`), p1.ID, golang.ID); err != nil { + t.Fatalf("attach golang: %v", err) + } + merged, err := s.MergeTags(golang.ID, goID) + if err != nil { + t.Fatalf("merge: %v", err) + } + if merged.ID != goID { + t.Errorf("merge should return target tag, got %d want %d", merged.ID, goID) + } + if merged.Color != "#3d7f9c" { + t.Errorf("target tag color lost, got %q", merged.Color) + } + + // Dashboard + d, err := s.Dashboard() + if err != nil { + t.Fatalf("dashboard: %v", err) + } + if d.TotalPosts != 2 { + t.Errorf("total_posts = %d", d.TotalPosts) + } + if d.PublishedPosts != 2 { + t.Errorf("published_posts = %d", d.PublishedPosts) + } + if len(d.RecentPosts) == 0 { + t.Errorf("recent posts empty") + } + if len(d.TopTags) == 0 { + t.Errorf("top tags empty") + } +} + +func openTestStore(t *testing.T) *Store { + t.Helper() + d, err := db.Open("sqlite", ":memory:") + if err != nil { + t.Fatalf("open sqlite: %v", err) + } + s, err := New(d) + if err != nil { + t.Fatalf("new store: %v", err) + } + return s +} diff --git a/frontend/index.html b/frontend/index.html index 91b6ffa..5aa9198 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,6 +3,8 @@ + + ONE · 一个博客 diff --git a/frontend/package.json b/frontend/package.json index 35c1002..a094bd8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "dependencies": { "@egoist/tailwindcss-icons": "^1.8.0", "@iconify-json/mingcute": "^1.1.17", + "@milkdown/crepe": "^7.22.1", "dompurify": "^3.1.6", "marked": "^12.0.2", "vue": "^3.4.0", @@ -28,6 +29,8 @@ "vue-tsc": "^2.1.0" }, "pnpm": { - "onlyBuiltDependencies": ["esbuild"] + "onlyBuiltDependencies": [ + "esbuild" + ] } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index c5ede1b..a3b00d4 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@iconify-json/mingcute': specifier: ^1.1.17 version: 1.2.7 + '@milkdown/crepe': + specifier: ^7.22.1 + version: 7.22.1(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.4)(typescript@5.9.3) dompurify: specifier: ^3.1.6 version: 3.4.12 @@ -75,6 +78,99 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@codemirror/autocomplete@6.20.3': + resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} + + '@codemirror/commands@6.11.1': + resolution: {integrity: sha512-O/4hG3SC1YwcmQ0d2UVNDs+AsaNWd1iHVxbTeEBuqH+6bExAiPK3iS/BvpY6rZGURALv4ZD3sIgcCmRvw3ehBg==} + + '@codemirror/lang-angular@0.1.4': + resolution: {integrity: sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==} + + '@codemirror/lang-cpp@6.0.3': + resolution: {integrity: sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==} + + '@codemirror/lang-css@6.3.1': + resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} + + '@codemirror/lang-go@6.0.1': + resolution: {integrity: sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==} + + '@codemirror/lang-html@6.4.12': + resolution: {integrity: sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w==} + + '@codemirror/lang-java@6.0.2': + resolution: {integrity: sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==} + + '@codemirror/lang-javascript@6.2.5': + resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==} + + '@codemirror/lang-jinja@6.0.1': + resolution: {integrity: sha512-P5kyHLObzjtbGj16h+hyvZTxJhSjBEeSx4wMjbnAf3b0uwTy2+F0zGjMZL4PQOm/mh2eGZ5xUDVZXgwP783Nsw==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/lang-less@6.0.2': + resolution: {integrity: sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==} + + '@codemirror/lang-liquid@6.3.2': + resolution: {integrity: sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw==} + + '@codemirror/lang-markdown@6.5.2': + resolution: {integrity: sha512-AwBOdkWYuA//WcM0xO5PfHPUcmz/O2i5o0Nsg1U69SII/loCJlFI1Romd9xp2HYb1kYJRGZotyqRghuHH5n8Kw==} + + '@codemirror/lang-php@6.0.2': + resolution: {integrity: sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==} + + '@codemirror/lang-python@6.2.1': + resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==} + + '@codemirror/lang-rust@6.0.2': + resolution: {integrity: sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==} + + '@codemirror/lang-sass@6.0.2': + resolution: {integrity: sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==} + + '@codemirror/lang-sql@6.10.0': + resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==} + + '@codemirror/lang-vue@0.1.3': + resolution: {integrity: sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==} + + '@codemirror/lang-wast@6.0.2': + resolution: {integrity: sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==} + + '@codemirror/lang-xml@6.1.0': + resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==} + + '@codemirror/lang-yaml@6.1.3': + resolution: {integrity: sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==} + + '@codemirror/language-data@6.5.2': + resolution: {integrity: sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==} + + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} + + '@codemirror/legacy-modes@6.5.4': + resolution: {integrity: sha512-/cZr6qZyl08iYNLGsJ862CXXNI51LryRFRE40ejgoIjXZz0C1rGkD3/Ek5jM/8w1ceRjqtt4qx/KLMh4zBTgew==} + + '@codemirror/lint@6.9.7': + resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} + + '@codemirror/search@6.7.2': + resolution: {integrity: sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA==} + + '@codemirror/state@6.7.5': + resolution: {integrity: sha512-QjLbZmY1Au3JiRrDVYFLRD0BZ3SOKS9pR3yjIkd7u27YY8TFD9/Q9fhPnLV5l1mHFSo3hHU/N31vpwEJOx4owQ==} + + '@codemirror/theme-one-dark@6.1.3': + resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} + + '@codemirror/view@6.43.12': + resolution: {integrity: sha512-Nv0vxQ19NAqvB/c2pFzjIzFlzzJl7jmdtNkwOwGbn0Ks9mFAzibvumz7cQem5cRsFA2cEw2fg+uHZGbcHupLQQ==} + '@egoist/tailwindcss-icons@1.9.2': resolution: {integrity: sha512-I6XsSykmhu2cASg5Hp/ICLsJ/K/1aXPaSKjgbWaNp2xYnb4We/arWMmkhhV+9CglOFCUbqx0A3mM2kWV32ZIhw==} peerDependencies: @@ -218,6 +314,15 @@ packages: cpu: [x64] os: [win32] + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + '@iconify-json/mingcute@1.2.7': resolution: {integrity: sha512-bp4z6F53KAQp99aSL7qPCHL7HWUNGMNKIhfo/dx9pwVHiad/WusUbTOFXEZfpA44syJPzSOxa3O6Pwiq7qZz1g==} @@ -240,6 +345,133 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/cpp@1.1.6': + resolution: {integrity: sha512-vh9gWWJOXFVY8HBHK3Twzq8MgwG2iN4GSyzBP9sCGTe37P15x2R14VaBQk0VA0ezTRN1KHYBBsHhvpGZ2Xy/pA==} + + '@lezer/css@1.3.6': + resolution: {integrity: sha512-YJE78Wcg+zX8f10hiHWQ4Az48Qr/c13eId0VtRQYLBpxHDmDeSrXIlkbl+fJGW42rWC/uoUco9mhBZeVWP/A1g==} + + '@lezer/go@1.0.1': + resolution: {integrity: sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/html@1.3.13': + resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==} + + '@lezer/java@1.1.4': + resolution: {integrity: sha512-gQylJ2xHV2xXsVqtnAcJrcROvLZJaQiC0tR//+q0k961BxqvyYttusPkJ8d9T1fPDJKHJW2etUH8XbIn9jhVUg==} + + '@lezer/javascript@1.5.5': + resolution: {integrity: sha512-sWg4yX1J6XW67AaAynVt0iwF0M5c+np36TEu+P2ifAJ8haRYvHnWDV28r1jdwnJehWCwXECutAUy56K4RBZIyg==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + + '@lezer/markdown@1.7.2': + resolution: {integrity: sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ==} + + '@lezer/php@1.0.6': + resolution: {integrity: sha512-QJ2xNXPmsm/Z3IpThq8fnJrEIVpxhsIoj6GZBW0JYEd/l9zxpJZW216X4fzqQY4vgpdGIumypvI4uQjd4tnYtw==} + + '@lezer/python@1.1.19': + resolution: {integrity: sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ==} + + '@lezer/rust@1.0.3': + resolution: {integrity: sha512-XxErOjZzQ7yJt1agUT4fu9qQvESZ3acgoxpPaPTPOiUx+duCjaVAtZGFIgphkHxlN05djdVAIOy/wItShMEjqQ==} + + '@lezer/sass@1.1.0': + resolution: {integrity: sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==} + + '@lezer/xml@1.0.6': + resolution: {integrity: sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==} + + '@lezer/yaml@1.0.4': + resolution: {integrity: sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==} + + '@marijn/find-cluster-break@1.0.4': + resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==} + + '@milkdown/components@7.22.1': + resolution: {integrity: sha512-6IA8fcFcBTm/x1X1typz73yUoT1JuN4srmifGAIeWEtCnayEwRjFxpQOoQrfvMgBYx4DSHcPVLhJUlR/xbBtxg==} + peerDependencies: + '@codemirror/language': ^6 + '@codemirror/state': ^6 + '@codemirror/view': ^6 + + '@milkdown/core@7.22.1': + resolution: {integrity: sha512-X4O3al2aYugpFP/Do7VoPgvVEftjJVi+6UhZmz9fSdqMtpU70AitJ8R/erUA18hay2hWvNO9m3pxIJoR6no6KQ==} + + '@milkdown/crepe@7.22.1': + resolution: {integrity: sha512-0BZTGBRV8lnlKsA3vmPK5Qyomjqd952493AIzfnbhiYwzHpluX03iMtOgHC6E2UcpMXP86P5CXhPUWjzDu+JZQ==} + + '@milkdown/ctx@7.22.1': + resolution: {integrity: sha512-6+LJdo3DcrkO1lzGcje0yWqaG6ZKgTR8kDxyDrrm7iocBjq3M+bkw04f99ZG/BLp1Rbc4yRyin2mUy6m4SEVaA==} + + '@milkdown/exception@7.22.1': + resolution: {integrity: sha512-wXUfn+fpWy0jeJ6AVEjKPieouLfJIDLQ4IRqyEL3d5yZDiZTtKu1Va0+RprrvqANpGJ1YBRQ1uCgDE6sJgTEjQ==} + + '@milkdown/kit@7.22.1': + resolution: {integrity: sha512-gXLMhjqe0j8XRSUe97LBXbpZ0sDi5EM6nnt1zXewUaGDS3mleMjNntSCWrp9ln40kz8tGs0ol50kSAr1n/JBnQ==} + + '@milkdown/plugin-block@7.22.1': + resolution: {integrity: sha512-R0gOuKRqR1DyFULD75Oic0+57DXTthagUZbbB9lsHM59aQmxKJ1nRmujptPeMfDlDfEod2ao5rsow7LO8p+ZWQ==} + + '@milkdown/plugin-clipboard@7.22.1': + resolution: {integrity: sha512-H1meyLEj2fOnFWSopeQ2H7hl6VXM+OQZoU9H7GrlHFM5GKtBFBErFFV4Cl5tUSVFY+xYy/4rEPOA23Ba1u6OfA==} + + '@milkdown/plugin-cursor@7.22.1': + resolution: {integrity: sha512-3tJzpQl5cyn+8vzsHkPBYsW6QacEN7yP2O5GkyUjrNLvMIQ5vVi7GUuztLeoouJKlOc1vjIq3h7ARXCgHAKe8g==} + + '@milkdown/plugin-diff@7.22.1': + resolution: {integrity: sha512-qjuCMx11HwhMlTJuybqbFZT8PQkasSi0qICxq2HdYqH0WQuLO99I2mb17UAJz1aF+JM9rp1StHWr4bxC0ofamA==} + + '@milkdown/plugin-history@7.22.1': + resolution: {integrity: sha512-SRD6emVhfVmA6mrcCsCiWrONN/2Kp+VT5LLojIQxNc/spzjWw15PtkA3r4nUe9cZmNN5rE7XvrPaA0WnjJIyeQ==} + + '@milkdown/plugin-indent@7.22.1': + resolution: {integrity: sha512-s/wmqxaJpIKT01gEXsC9gCkzA3Clm+MNioZLzNCYD8xI/4Hf3AhmIEoEqf3LIQ9w10Zs2yhIxIrknNWfCOgsEw==} + + '@milkdown/plugin-listener@7.22.1': + resolution: {integrity: sha512-6k8JMDrdAL0E0WygpDX1YOR0Q9Wc4pia8pIMN5nN5liEeAyNaM6LwAxXJY+awQJ0OstDKHTfjkh7WzJo4QiXOA==} + + '@milkdown/plugin-slash@7.22.1': + resolution: {integrity: sha512-eFKCjfgXfAQusTSOyzhz4nASquOApdiBy9hnGkPB/SJ6oltTC8SonB3nIxF/X0wPZHfn09vuz/JBqh9cyDtXJA==} + + '@milkdown/plugin-streaming@7.22.1': + resolution: {integrity: sha512-CR0lujyc7ae0sbjNfscvS+b+cUP7b6XJ90ioTor605UV+F8EKh5t4PMJM79IVzSUCeob51a+rtXA2LAA1b/GyA==} + + '@milkdown/plugin-tooltip@7.22.1': + resolution: {integrity: sha512-drdWA/7WlrDr2B+ABYf4tY9xTiwg/CJMUCydfD05dR2d8wOkaF6oOHZwJbWnkWLMtAJWdieOYgfXGPhQRwXxCg==} + + '@milkdown/plugin-trailing@7.22.1': + resolution: {integrity: sha512-Buy8IX3I7wwq0QPfJRep77QnvlkBeg6BvDHkbh5eTMrgnwSiWhN+tV9RVCYpdWXM7QhQZpSjU2SejluLuTyVCg==} + + '@milkdown/plugin-upload@7.22.1': + resolution: {integrity: sha512-vV0bZzdh/PhM+Jk9Nk5cKHDXHY5jt+ePcaitTfvESDYfg83YZ9AglOhw/YclPWklMvswrnCGZZFTZD4daqc4hg==} + + '@milkdown/preset-commonmark@7.22.1': + resolution: {integrity: sha512-it+G0YUG5MDXt0qLB6W083ss5i6tnWAXWCW8SgnmsehGxntkN5//gNM/8vh3rqwl8RvnBvjLWpJ6TgHSlBPhlw==} + + '@milkdown/preset-gfm@7.22.1': + resolution: {integrity: sha512-UPMdHRdMHlVourOOTiwsp3qHu614pDFWlWtVzxe8fyl8uKZUk6IOuewH5ubhqD/opos8OEB+zgEomUGfee1oTw==} + + '@milkdown/prose@7.22.1': + resolution: {integrity: sha512-fqgTHl94G7oDPY94cPYBm6ARxEoLQ10ubkTNLD1nzAlTUfJLULZkIEMSMy4CECncw2go3ExW32CZPpXLJVSxnQ==} + + '@milkdown/transformer@7.22.1': + resolution: {integrity: sha512-rU1IBtxezNg7TSWaq+RP59/t6tfD3jIPGrwAtMKPIGuYEax+8d1fyGisp98TeI3l0VQEIgMpF+KlNDV+Y1HJVA==} + + '@milkdown/utils@7.22.1': + resolution: {integrity: sha512-ye0LGy/Ez8fjtcYyYj93wp4XfH00JYpCiy1IDHYB+9HVIpnbr3ed7PvY4s2000xmDWe0Vlx07TOyv96yvFrnjQ==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -252,6 +484,9 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@ocavue/utils@1.8.0': + resolution: {integrity: sha512-BD1ro+6Kllkmr/at+o32kvnLrVooUbk9gw3JAPaoAZXx+A5GhZ6Laa+UA38ifhG3vaamx+jcgEwX22BmTSMUVA==} + '@rollup/rollup-android-arm-eabi@4.62.3': resolution: {integrity: sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==} cpu: [arm] @@ -390,12 +625,36 @@ packages: cpu: [x64] os: [win32] + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + + '@types/katex@0.16.8': + resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.25': + resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@vitejs/plugin-vue@5.2.4': resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -473,6 +732,9 @@ packages: peerDependencies: postcss: ^8.1.0 + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -504,14 +766,38 @@ packages: caniuse-lite@1.0.30001806: resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -523,6 +809,25 @@ packages: de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -552,9 +857,16 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -625,10 +937,22 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + katex@0.16.47: + resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} + hasBin: true + + katex@0.18.7: + resolution: {integrity: sha512-h+UCwkZ+4Jz8WQ7MLGfj7UVFrRCizGb912fwF4luGdYsC5paYG1vx+jy+KRcC/XkpjGva/P7nAWuxNnPzRvzHw==} + hasBin: true + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -636,18 +960,153 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@12.0.2: resolution: {integrity: sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==} engines: {node: '>= 18'} hasBin: true + mdast-util-definitions@6.0.0: + resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-math@3.0.0: + resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.2: + resolution: {integrity: sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-math@3.1.0: + resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -656,6 +1115,9 @@ packages: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} @@ -667,6 +1129,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@6.0.1: + resolution: {integrity: sha512-3wVS3i51pE2pi1k5FFL/95BGfVS0kSsvDVuGXHOtxox/TywUmtgq+3qiTOTbs9J7KfHaXPiN171k/A6dBnaXFw==} + engines: {node: ^22 || ^24 || >=26} + hasBin: true + node-releases@2.0.51: resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} engines: {node: '>=18'} @@ -683,6 +1150,9 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + package-manager-detector@1.8.0: resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} @@ -758,6 +1228,65 @@ packages: resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} + prosemirror-changeset@2.4.3: + resolution: {integrity: sha512-J8jqUZ1xybXKY44v3K0c8m8ZTIqJPVT34t2UVyxv4VEWoNd5C9t5Y52GNuMriRJ3IpNxOSE2R60vN4qBrTMcyQ==} + + prosemirror-commands@1.7.2: + resolution: {integrity: sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==} + + prosemirror-drop-indicator@0.1.4: + resolution: {integrity: sha512-YaRB1pZmU5GCorPVWbc9dbhbwqr4iMBO/AjPu4BTKHCUzxEDUXje2dUyoxOHib/z4uyPUZTJz64h7mHDJZeSzA==} + + prosemirror-dropcursor@1.8.3: + resolution: {integrity: sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==} + + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-model@1.25.11: + resolution: {integrity: sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==} + + prosemirror-safari-ime-span@1.0.2: + resolution: {integrity: sha512-QJqD8s1zE/CuK56kDsUhndh5hiHh/gFnAuPOA9ytva2s85/ZEt2tNWeALTJN48DtWghSKOmiBsvVn2OlnJ5H2w==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-transform@1.12.1: + resolution: {integrity: sha512-t4F5615FycnCqsX7ShTUs8+jfnwcf46kuFRvSl/3qFx2QTZ4DjgowesLHQXcFP7G//TjesqZG3WtgL7vdyVJyA==} + + prosemirror-view@1.42.4: + resolution: {integrity: sha512-H/LErnE8Vms1GYkvhfj6G3K9rc2p+o5EHGmTwvPOl0f21wPPxlMVRB8ICOseH+COb2oPKFEoufbgY6yet/dR4w==} + + prosemirror-virtual-cursor@0.4.2: + resolution: {integrity: sha512-pUMKnIuOhhnMcgIJUjhIQTVJruBEGxfMBVQSrK0g2qhGPDm1i12KdsVaFw15dYk+29tZcxjMeR7P5VDKwmbwJg==} + peerDependencies: + prosemirror-model: ^1.0.0 + prosemirror-state: ^1.0.0 + prosemirror-view: ^1.0.0 + peerDependenciesMeta: + prosemirror-model: + optional: true + prosemirror-state: + optional: true + prosemirror-view: + optional: true + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -768,6 +1297,24 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-inline-links@7.0.0: + resolution: {integrity: sha512-4uj1pPM+F495ySZhTIB6ay2oSkTsKgmYaKk/q5HIdhX2fuyLEegpjWa0VdJRJ01sgOqAFo7MBKdDUejIYBMVMQ==} + + remark-math@6.0.0: + resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + remark@15.0.1: + resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} @@ -782,6 +1329,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -789,6 +1339,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + style-mod@4.1.4: + resolution: {integrity: sha512-XXWIQt633/EpAFx8aZDOTjBzrCaGmhvEQlQo6MVPfa2OzO2cWo+4hV9h+6UkHYlXGfy+ODXKUdP7Pthmcu5ATw==} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -822,6 +1375,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -830,6 +1386,24 @@ packages: engines: {node: '>=14.17'} hasBin: true + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -839,6 +1413,12 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite@5.4.21: resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} engines: {node: ^18.0.0 || >=20.0.0} @@ -892,6 +1472,12 @@ packages: typescript: optional: true + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -914,6 +1500,264 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@codemirror/autocomplete@6.20.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.5 + '@codemirror/view': 6.43.12 + '@lezer/common': 1.5.2 + + '@codemirror/commands@6.11.1': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.5 + '@codemirror/view': 6.43.12 + '@lezer/common': 1.5.2 + + '@codemirror/lang-angular@0.1.4': + dependencies: + '@codemirror/lang-html': 6.4.12 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.4 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-cpp@6.0.3': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/cpp': 1.1.6 + + '@codemirror/lang-css@6.3.1': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.5 + '@lezer/common': 1.5.2 + '@lezer/css': 1.3.6 + + '@codemirror/lang-go@6.0.1': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.5 + '@lezer/common': 1.5.2 + '@lezer/go': 1.0.1 + + '@codemirror/lang-html@6.4.12': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.5 + '@codemirror/view': 6.43.12 + '@lezer/common': 1.5.2 + '@lezer/css': 1.3.6 + '@lezer/html': 1.3.13 + + '@codemirror/lang-java@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/java': 1.1.4 + + '@codemirror/lang-javascript@6.2.5': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/state': 6.7.5 + '@codemirror/view': 6.43.12 + '@lezer/common': 1.5.2 + '@lezer/javascript': 1.5.5 + + '@codemirror/lang-jinja@6.0.1': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/lang-html': 6.4.12 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.5 + '@codemirror/view': 6.43.12 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/json': 1.0.3 + + '@codemirror/lang-less@6.0.2': + dependencies: + '@codemirror/lang-css': 6.3.1 + '@codemirror/language': 6.12.4 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-liquid@6.3.2': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/lang-html': 6.4.12 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.5 + '@codemirror/view': 6.43.12 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-markdown@6.5.2': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/lang-html': 6.4.12 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.5 + '@codemirror/view': 6.43.12 + '@lezer/common': 1.5.2 + '@lezer/markdown': 1.7.2 + + '@codemirror/lang-php@6.0.2': + dependencies: + '@codemirror/lang-html': 6.4.12 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.5 + '@lezer/common': 1.5.2 + '@lezer/php': 1.0.6 + + '@codemirror/lang-python@6.2.1': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.5 + '@lezer/common': 1.5.2 + '@lezer/python': 1.1.19 + + '@codemirror/lang-rust@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/rust': 1.0.3 + + '@codemirror/lang-sass@6.0.2': + dependencies: + '@codemirror/lang-css': 6.3.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.5 + '@lezer/common': 1.5.2 + '@lezer/sass': 1.1.0 + + '@codemirror/lang-sql@6.10.0': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.5 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-vue@0.1.3': + dependencies: + '@codemirror/lang-html': 6.4.12 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.4 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-wast@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-xml@6.1.0': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.5 + '@codemirror/view': 6.43.12 + '@lezer/common': 1.5.2 + '@lezer/xml': 1.0.6 + + '@codemirror/lang-yaml@6.1.3': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.5 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + '@lezer/yaml': 1.0.4 + + '@codemirror/language-data@6.5.2': + dependencies: + '@codemirror/lang-angular': 0.1.4 + '@codemirror/lang-cpp': 6.0.3 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-go': 6.0.1 + '@codemirror/lang-html': 6.4.12 + '@codemirror/lang-java': 6.0.2 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/lang-jinja': 6.0.1 + '@codemirror/lang-json': 6.0.2 + '@codemirror/lang-less': 6.0.2 + '@codemirror/lang-liquid': 6.3.2 + '@codemirror/lang-markdown': 6.5.2 + '@codemirror/lang-php': 6.0.2 + '@codemirror/lang-python': 6.2.1 + '@codemirror/lang-rust': 6.0.2 + '@codemirror/lang-sass': 6.0.2 + '@codemirror/lang-sql': 6.10.0 + '@codemirror/lang-vue': 0.1.3 + '@codemirror/lang-wast': 6.0.2 + '@codemirror/lang-xml': 6.1.0 + '@codemirror/lang-yaml': 6.1.3 + '@codemirror/language': 6.12.4 + '@codemirror/legacy-modes': 6.5.4 + + '@codemirror/language@6.12.4': + dependencies: + '@codemirror/state': 6.7.5 + '@codemirror/view': 6.43.12 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.4 + + '@codemirror/legacy-modes@6.5.4': + dependencies: + '@codemirror/language': 6.12.4 + + '@codemirror/lint@6.9.7': + dependencies: + '@codemirror/state': 6.7.5 + '@codemirror/view': 6.43.12 + crelt: 1.0.7 + + '@codemirror/search@6.7.2': + dependencies: + '@codemirror/state': 6.7.5 + '@codemirror/view': 6.43.12 + crelt: 1.0.7 + + '@codemirror/state@6.7.5': + dependencies: + '@marijn/find-cluster-break': 1.0.4 + + '@codemirror/theme-one-dark@6.1.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.5 + '@codemirror/view': 6.43.12 + '@lezer/highlight': 1.2.3 + + '@codemirror/view@6.43.12': + dependencies: + '@codemirror/state': 6.7.5 + crelt: 1.0.7 + style-mod: 4.1.4 + w3c-keyname: 2.2.8 + '@egoist/tailwindcss-icons@1.9.2(tailwindcss@3.4.19)': dependencies: '@iconify/utils': 3.1.4 @@ -988,6 +1832,17 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/utils@0.2.12': {} + '@iconify-json/mingcute@1.2.7': dependencies: '@iconify/types': 2.0.0 @@ -1014,6 +1869,384 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lezer/common@1.5.2': {} + + '@lezer/cpp@1.1.6': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/css@1.3.6': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/go@1.0.1': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/html@1.3.13': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/java@1.1.4': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/javascript@1.5.5': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/markdown@1.7.2': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + + '@lezer/php@1.0.6': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/python@1.1.19': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/rust@1.0.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/sass@1.1.0': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/xml@1.0.6': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/yaml@1.0.4': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@marijn/find-cluster-break@1.0.4': {} + + '@milkdown/components@7.22.1(@codemirror/language@6.12.4)(@codemirror/state@6.7.5)(@codemirror/view@6.43.12)(typescript@5.9.3)': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.5 + '@codemirror/view': 6.43.12 + '@floating-ui/dom': 1.8.0 + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/exception': 7.22.1 + '@milkdown/plugin-diff': 7.22.1 + '@milkdown/plugin-tooltip': 7.22.1 + '@milkdown/preset-commonmark': 7.22.1 + '@milkdown/preset-gfm': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/transformer': 7.22.1 + '@milkdown/utils': 7.22.1 + '@types/lodash-es': 4.17.12 + clsx: 2.1.1 + dompurify: 3.4.12 + lodash-es: 4.18.1 + nanoid: 6.0.1 + unist-util-visit: 5.1.0 + vue: 3.5.40(typescript@5.9.3) + transitivePeerDependencies: + - supports-color + - typescript + + '@milkdown/core@7.22.1': + dependencies: + '@milkdown/ctx': 7.22.1 + '@milkdown/exception': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/transformer': 7.22.1 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + '@milkdown/crepe@7.22.1(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.4)(typescript@5.9.3)': + dependencies: + '@codemirror/commands': 6.11.1 + '@codemirror/language': 6.12.4 + '@codemirror/language-data': 6.5.2 + '@codemirror/state': 6.7.5 + '@codemirror/theme-one-dark': 6.1.3 + '@codemirror/view': 6.43.12 + '@milkdown/kit': 7.22.1(@codemirror/language@6.12.4)(@codemirror/state@6.7.5)(@codemirror/view@6.43.12)(typescript@5.9.3) + '@types/lodash-es': 4.17.12 + clsx: 2.1.1 + codemirror: 6.0.2 + dompurify: 3.4.12 + katex: 0.18.7 + lodash-es: 4.18.1 + prosemirror-virtual-cursor: 0.4.2(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.4) + remark-math: 6.0.0 + unist-util-visit: 5.1.0 + vue: 3.5.40(typescript@5.9.3) + transitivePeerDependencies: + - prosemirror-model + - prosemirror-state + - prosemirror-view + - supports-color + - typescript + + '@milkdown/ctx@7.22.1': + dependencies: + '@milkdown/exception': 7.22.1 + + '@milkdown/exception@7.22.1': {} + + '@milkdown/kit@7.22.1(@codemirror/language@6.12.4)(@codemirror/state@6.7.5)(@codemirror/view@6.43.12)(typescript@5.9.3)': + dependencies: + '@milkdown/components': 7.22.1(@codemirror/language@6.12.4)(@codemirror/state@6.7.5)(@codemirror/view@6.43.12)(typescript@5.9.3) + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/exception': 7.22.1 + '@milkdown/plugin-block': 7.22.1 + '@milkdown/plugin-clipboard': 7.22.1 + '@milkdown/plugin-cursor': 7.22.1 + '@milkdown/plugin-diff': 7.22.1 + '@milkdown/plugin-history': 7.22.1 + '@milkdown/plugin-indent': 7.22.1 + '@milkdown/plugin-listener': 7.22.1 + '@milkdown/plugin-slash': 7.22.1 + '@milkdown/plugin-streaming': 7.22.1 + '@milkdown/plugin-tooltip': 7.22.1 + '@milkdown/plugin-trailing': 7.22.1 + '@milkdown/plugin-upload': 7.22.1 + '@milkdown/preset-commonmark': 7.22.1 + '@milkdown/preset-gfm': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/transformer': 7.22.1 + '@milkdown/utils': 7.22.1 + transitivePeerDependencies: + - '@codemirror/language' + - '@codemirror/state' + - '@codemirror/view' + - supports-color + - typescript + + '@milkdown/plugin-block@7.22.1': + dependencies: + '@floating-ui/dom': 1.8.0 + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + '@types/lodash-es': 4.17.12 + lodash-es: 4.18.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-clipboard@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-cursor@7.22.1': + dependencies: + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + prosemirror-drop-indicator: 0.1.4 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-diff@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/transformer': 7.22.1 + '@milkdown/utils': 7.22.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-history@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-indent@7.22.1': + dependencies: + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-listener@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@types/lodash-es': 4.17.12 + lodash-es: 4.18.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-slash@7.22.1': + dependencies: + '@floating-ui/dom': 1.8.0 + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + '@types/lodash-es': 4.17.12 + lodash-es: 4.18.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-streaming@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/plugin-diff': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-tooltip@7.22.1': + dependencies: + '@floating-ui/dom': 1.8.0 + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + '@types/lodash-es': 4.17.12 + lodash-es: 4.18.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-trailing@7.22.1': + dependencies: + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-upload@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/exception': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/preset-commonmark@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/exception': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/transformer': 7.22.1 + '@milkdown/utils': 7.22.1 + remark-inline-links: 7.0.0 + unist-util-visit: 5.1.0 + unist-util-visit-parents: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@milkdown/preset-gfm@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/exception': 7.22.1 + '@milkdown/preset-commonmark': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/transformer': 7.22.1 + '@milkdown/utils': 7.22.1 + prosemirror-safari-ime-span: 1.0.2 + remark-gfm: 4.0.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/prose@7.22.1': + dependencies: + '@milkdown/exception': 7.22.1 + prosemirror-changeset: 2.4.3 + prosemirror-commands: 1.7.2 + prosemirror-dropcursor: 1.8.3 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-transform: 1.12.1 + prosemirror-view: 1.42.4 + + '@milkdown/transformer@7.22.1': + dependencies: + '@milkdown/exception': 7.22.1 + '@milkdown/prose': 7.22.1 + remark: 15.0.1 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + '@milkdown/utils@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/exception': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/transformer': 7.22.1 + nanoid: 6.0.1 + transitivePeerDependencies: + - supports-color + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -1026,6 +2259,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@ocavue/utils@1.8.0': {} + '@rollup/rollup-android-arm-eabi@4.62.3': optional: true @@ -1101,11 +2336,35 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.3': optional: true + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + '@types/estree@1.0.9': {} + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + + '@types/katex@0.16.8': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.25 + + '@types/lodash@4.17.25': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/trusted-types@2.0.7': optional: true + '@types/unist@3.0.3': {} + '@vitejs/plugin-vue@5.2.4(vite@5.4.21)(vue@3.5.40(typescript@5.9.3))': dependencies: vite: 5.4.21 @@ -1217,6 +2476,8 @@ snapshots: postcss: 8.5.25 postcss-value-parser: 4.2.0 + bail@2.0.2: {} + balanced-match@1.0.2: {} baseline-browser-mapping@2.11.8: {} @@ -1243,6 +2504,10 @@ snapshots: caniuse-lite@1.0.30001806: {} + ccount@2.0.1: {} + + character-entities@2.0.2: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -1255,14 +2520,46 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + clsx@2.1.1: {} + + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/commands': 6.11.1 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/search': 6.7.2 + '@codemirror/state': 6.7.5 + '@codemirror/view': 6.43.12 + + commander@15.0.0: {} + commander@4.1.1: {} + commander@8.3.0: {} + + crelt@1.0.7: {} + cssesc@3.0.0: {} csstype@3.2.3: {} de-indent@1.0.2: {} + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + didyoumean@1.2.2: {} dlv@1.1.3: {} @@ -1305,8 +2602,12 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@5.0.0: {} + estree-walker@2.0.2: {} + extend@3.0.2: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -1366,20 +2667,357 @@ snapshots: is-number@7.0.0: {} + is-plain-obj@4.1.0: {} + jiti@1.21.7: {} + katex@0.16.47: + dependencies: + commander: 8.3.0 + + katex@0.18.7: + dependencies: + commander: 15.0.0 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} + lodash-es@4.18.1: {} + + longest-streak@3.1.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + markdown-table@3.0.4: {} + marked@12.0.2: {} + mdast-util-definitions@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-math@3.0.0: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + longest-streak: 3.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + unist-util-remove-position: 5.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.2: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.2 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-math@3.1.0: + dependencies: + '@types/katex': 0.16.8 + devlop: 1.1.0 + katex: 0.16.47 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -1389,6 +3027,8 @@ snapshots: dependencies: brace-expansion: 2.1.4 + ms@2.1.3: {} + muggle-string@0.4.1: {} mz@2.7.0: @@ -1399,6 +3039,8 @@ snapshots: nanoid@3.3.16: {} + nanoid@6.0.1: {} + node-releases@2.0.51: {} normalize-path@3.0.0: {} @@ -1407,6 +3049,8 @@ snapshots: object-hash@3.0.0: {} + orderedmap@2.1.1: {} + package-manager-detector@1.8.0: {} path-browserify@1.0.1: {} @@ -1460,6 +3104,98 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prosemirror-changeset@2.4.3: + dependencies: + prosemirror-transform: 1.12.1 + + prosemirror-commands@1.7.2: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.1 + + prosemirror-drop-indicator@0.1.4: + dependencies: + '@ocavue/utils': 1.8.0 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-view: 1.42.4 + + prosemirror-dropcursor@1.8.3: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.1 + prosemirror-view: 1.42.4 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-view: 1.42.4 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.1 + prosemirror-view: 1.42.4 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.1 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-model@1.25.11: + dependencies: + orderedmap: 2.1.1 + + prosemirror-safari-ime-span@1.0.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-view: 1.42.4 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.1 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-transform: 1.12.1 + prosemirror-view: 1.42.4 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.1 + prosemirror-view: 1.42.4 + + prosemirror-transform@1.12.1: + dependencies: + prosemirror-model: 1.25.11 + + prosemirror-view@1.42.4: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.1 + + prosemirror-virtual-cursor@0.4.2(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.4): + optionalDependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-view: 1.42.4 + queue-microtask@1.2.3: {} read-cache@1.0.0: @@ -1470,6 +3206,56 @@ snapshots: dependencies: picomatch: 2.3.2 + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-inline-links@7.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-definitions: 6.0.0 + unist-util-visit: 5.1.0 + + remark-math@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-math: 3.0.0 + micromark-extension-math: 3.1.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + remark@15.0.1: + dependencies: + '@types/mdast': 4.0.4 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + resolve@1.22.12: dependencies: es-errors: 1.3.0 @@ -1510,12 +3296,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.3 fsevents: 2.3.3 + rope-sequence@1.3.4: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 source-map-js@1.2.1: {} + style-mod@4.1.4: {} + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -1575,10 +3365,46 @@ snapshots: dependencies: is-number: 7.0.0 + trough@2.2.0: {} + ts-interface-checker@0.1.13: {} typescript@5.9.3: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + update-browserslist-db@1.2.3(browserslist@4.28.7): dependencies: browserslist: 4.28.7 @@ -1587,6 +3413,16 @@ snapshots: util-deprecate@1.0.2: {} + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite@5.4.21: dependencies: esbuild: 0.21.5 @@ -1617,3 +3453,7 @@ snapshots: '@vue/shared': 3.5.40 optionalDependencies: typescript: 5.9.3 + + w3c-keyname@2.2.8: {} + + zwitch@2.0.4: {} diff --git a/frontend/src/App.vue b/frontend/src/App.vue index bb5c4cd..d663e72 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,8 +1,41 @@ + + \ No newline at end of file diff --git a/frontend/src/admin/AdminLayout.vue b/frontend/src/admin/AdminLayout.vue index 153032a..ab56572 100644 --- a/frontend/src/admin/AdminLayout.vue +++ b/frontend/src/admin/AdminLayout.vue @@ -1,12 +1,32 @@ + \ No newline at end of file diff --git a/frontend/src/admin/CommandPalette.vue b/frontend/src/admin/CommandPalette.vue new file mode 100644 index 0000000..572d628 --- /dev/null +++ b/frontend/src/admin/CommandPalette.vue @@ -0,0 +1,133 @@ + + + \ No newline at end of file diff --git a/frontend/src/admin/DashboardView.vue b/frontend/src/admin/DashboardView.vue new file mode 100644 index 0000000..bc2aba8 --- /dev/null +++ b/frontend/src/admin/DashboardView.vue @@ -0,0 +1,140 @@ + + + \ No newline at end of file diff --git a/frontend/src/admin/EditorView.vue b/frontend/src/admin/EditorView.vue index e2b8b3a..c3bce07 100644 --- a/frontend/src/admin/EditorView.vue +++ b/frontend/src/admin/EditorView.vue @@ -1,8 +1,11 @@ - - + \ No newline at end of file diff --git a/frontend/src/admin/LoginView.vue b/frontend/src/admin/LoginView.vue index 15919c7..221a228 100644 --- a/frontend/src/admin/LoginView.vue +++ b/frontend/src/admin/LoginView.vue @@ -71,13 +71,14 @@ async function submit() { align-items: center; justify-content: center; padding: 40px 20px; + background: var(--admin-bg); } .card { width: 100%; max-width: 380px; - background: var(--card); - border: 1px solid var(--line); + background: var(--admin-card); + border: 1px solid var(--admin-line); border-radius: 4px; padding: 28px; } @@ -85,10 +86,11 @@ async function submit() { .title { font-size: 22px; margin: 6px 0 22px; + color: var(--admin-ink); } .error { - color: #9a5b45; + color: var(--admin-danger); font-size: 13px; margin: -6px 0 14px; } @@ -97,13 +99,13 @@ async function submit() { margin-top: 18px; font-size: 12.5px; line-height: 1.8; - color: var(--muted); + color: var(--admin-muted); } .hint code { font-family: var(--mono); font-size: 12px; - background: var(--paper-sunken); + background: var(--admin-paper-sunken); padding: 1px 4px; border-radius: 3px; } @@ -112,6 +114,6 @@ async function submit() { display: inline-block; margin-top: 14px; font-size: 13px; - color: var(--accent); + color: var(--admin-accent); } diff --git a/frontend/src/admin/PostsView.vue b/frontend/src/admin/PostsView.vue index c2070b3..1342ec4 100644 --- a/frontend/src/admin/PostsView.vue +++ b/frontend/src/admin/PostsView.vue @@ -1,33 +1,49 @@ - - + \ No newline at end of file diff --git a/frontend/src/admin/SettingsView.vue b/frontend/src/admin/SettingsView.vue index 31d3671..4e1a3e2 100644 --- a/frontend/src/admin/SettingsView.vue +++ b/frontend/src/admin/SettingsView.vue @@ -1,42 +1,47 @@ - - + \ No newline at end of file diff --git a/frontend/src/admin/ShortcutsPanel.vue b/frontend/src/admin/ShortcutsPanel.vue new file mode 100644 index 0000000..aded03f --- /dev/null +++ b/frontend/src/admin/ShortcutsPanel.vue @@ -0,0 +1,67 @@ + + + \ No newline at end of file diff --git a/frontend/src/admin/TagsView.vue b/frontend/src/admin/TagsView.vue index d69213e..5aaf725 100644 --- a/frontend/src/admin/TagsView.vue +++ b/frontend/src/admin/TagsView.vue @@ -5,176 +5,142 @@ import { adminApi } from '../api' const tags = ref([]) const loading = ref(true) const error = ref('') -const name = ref('') -const editing = ref(null) -const editName = ref('') +const newName = ref('') +const newColor = ref('') async function load() { loading.value = true - error.value = '' try { const data = await adminApi.tags() - tags.value = data.tags || [] + tags.value = data.tags || data || [] } catch (e) { error.value = e.message || '加载失败' } finally { loading.value = false } } - onMounted(load) async function create() { - if (!name.value.trim()) return + const name = newName.value.trim() + if (!name) return try { - await adminApi.createTag(name.value.trim()) - name.value = '' + await adminApi.createTag({ name, color: newColor.value }) + newName.value = '' + newColor.value = '' await load() } catch (e) { - error.value = e.message || '创建失败' + alert(e.message || '创建失败') } } -function startEdit(t) { - editing.value = t.id - editName.value = t.name -} - -async function saveEdit(id) { +async function update(t) { try { - await adminApi.renameTag(id, editName.value) - editing.value = null + await adminApi.updateTag(t.id, { name: t.name, color: t.color }) await load() } catch (e) { - error.value = e.message || '重命名失败' + alert(e.message || '更新失败') } } async function remove(t) { - if (!window.confirm(`删除标签「${t.name}」?文章上的关联会一起移除。`)) return + if (!window.confirm(`删除标签「${t.name}」?关联的所有文章会被取消关联。`)) return try { await adminApi.deleteTag(t.id) await load() } catch (e) { - error.value = e.message || '删除失败' + alert(e.message || '删除失败') + } +} + +const mergeTarget = ref(null) +const mergeInto = ref('') +const merging = ref(null) + +function startMerge(t) { + merging.value = t.id + mergeInto.value = '' +} +function cancelMerge() { + merging.value = null + mergeInto.value = '' +} + +async function doMerge(fromTag) { + const toId = Number(mergeInto.value) + if (!toId || toId === fromTag.id) { + alert('选一个不同的目标标签') + return + } + if (!window.confirm(`把「${fromTag.name}」合并到选中的标签?此操作不可撤销。`)) return + try { + await adminApi.mergeTag(fromTag.id, toId) + cancelMerge() + await load() + } catch (e) { + alert(e.message || '合并失败') } } - - + \ No newline at end of file diff --git a/frontend/src/api.js b/frontend/src/api.js index 3af8401..6ffc5f5 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -43,15 +43,20 @@ export const adminApi = { request('/api/admin/login', { method: 'POST', body: { username, password } }), logout: () => request('/api/admin/logout', { method: 'POST' }), me: () => request('/api/admin/me'), + dashboard: () => request('/api/admin/dashboard'), posts: (params = {}) => request('/api/admin/posts?' + new URLSearchParams(params)), post: (id) => request('/api/admin/posts/' + id), createPost: (body) => request('/api/admin/posts', { method: 'POST', body }), updatePost: (id, body) => request('/api/admin/posts/' + id, { method: 'PUT', body }), deletePost: (id) => request('/api/admin/posts/' + id, { method: 'DELETE' }), + bulkPosts: (ids, action) => + request('/api/admin/posts/bulk', { method: 'POST', body: { ids, action } }), tags: () => request('/api/admin/tags'), - createTag: (name) => request('/api/admin/tags', { method: 'POST', body: { name } }), - renameTag: (id, name) => request('/api/admin/tags/' + id, { method: 'PUT', body: { name } }), + createTag: (body) => request('/api/admin/tags', { method: 'POST', body }), + updateTag: (id, body) => request('/api/admin/tags/' + id, { method: 'PUT', body }), deleteTag: (id) => request('/api/admin/tags/' + id, { method: 'DELETE' }), + mergeTag: (id, toId) => + request('/api/admin/tags/' + id + '/merge', { method: 'POST', body: { to_id: toId } }), settings: () => request('/api/admin/settings'), saveSettings: (body) => request('/api/admin/settings', { method: 'PUT', body }) } diff --git a/frontend/src/components/LeftNav.vue b/frontend/src/components/LeftNav.vue index 6f83f2c..bb7a15b 100644 --- a/frontend/src/components/LeftNav.vue +++ b/frontend/src/components/LeftNav.vue @@ -2,6 +2,7 @@ import { site } from '../site' import { publicApi } from '../api' import { onMounted, ref } from 'vue' +import ThemeSwitcher from './ThemeSwitcher.vue' const tags = ref([]) @@ -39,8 +40,6 @@ onMounted(async () => { - 订阅 RSS -

常读标签

@@ -49,6 +48,11 @@ onMounted(async () => {
+ +
+

主题

+ +
@@ -130,21 +134,6 @@ onMounted(async () => { color: var(--accent); } -.subscribe { - align-self: flex-start; - padding: 7px 20px; - border-radius: 999px; - background: var(--accent); - color: #fff; - font-size: 14px; - border: 1px solid var(--accent); -} - -.subscribe:hover { - background: #356f88; - color: #fff; -} - .tagbox { border-top: 1px solid var(--line); padding-top: 14px; @@ -156,4 +145,13 @@ onMounted(async () => { gap: 6px; margin-top: 8px; } + +.theme-box { + border-top: 1px solid var(--line); + padding-top: 14px; +} + +.theme-box .theme-switcher { + margin-top: 8px; +} diff --git a/frontend/src/components/PostCard.vue b/frontend/src/components/PostCard.vue index 666f252..3e74fda 100644 --- a/frontend/src/components/PostCard.vue +++ b/frontend/src/components/PostCard.vue @@ -71,7 +71,7 @@ const initial = computed(() => (site.author_name || 'O').trim().slice(0, 1).toUp display: grid; grid-template-columns: 40px minmax(0, 1fr); gap: 12px; - padding: 18px 16px; + padding: 14px 16px; margin: 0 -16px; border-bottom: 1px solid var(--line-soft); transition: background 0.15s ease; diff --git a/frontend/src/components/ThemeSwitcher.vue b/frontend/src/components/ThemeSwitcher.vue new file mode 100644 index 0000000..9b0a05e --- /dev/null +++ b/frontend/src/components/ThemeSwitcher.vue @@ -0,0 +1,103 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/TopBar.vue b/frontend/src/components/TopBar.vue index b75778d..bd83681 100644 --- a/frontend/src/components/TopBar.vue +++ b/frontend/src/components/TopBar.vue @@ -29,7 +29,7 @@ import { site } from '../site' top: 0; z-index: 20; padding: 10px 18px; - background: rgba(250, 247, 241, 0.94); + background-color: var(--paper); backdrop-filter: blur(6px); border-bottom: 1px solid var(--line); } diff --git a/frontend/src/router.js b/frontend/src/router.js index e721b2a..743e2c9 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -13,7 +13,8 @@ const routes = [ path: '/admin', component: () => import('./admin/AdminLayout.vue'), children: [ - { path: '', name: 'admin-posts', component: () => import('./admin/PostsView.vue') }, + { path: '', name: 'admin-dashboard', component: () => import('./admin/DashboardView.vue') }, + { path: 'posts', name: 'admin-posts', component: () => import('./admin/PostsView.vue') }, { path: 'new', name: 'admin-new', component: () => import('./admin/EditorView.vue') }, { path: ':id', name: 'admin-edit', component: () => import('./admin/EditorView.vue') }, { path: 'tags', name: 'admin-tags', component: () => import('./admin/TagsView.vue') }, diff --git a/frontend/src/site.js b/frontend/src/site.js index 55692d5..1564f37 100644 --- a/frontend/src/site.js +++ b/frontend/src/site.js @@ -1,6 +1,52 @@ -import { reactive } from 'vue' +import { reactive, watch } from 'vue' import { publicApi } from './api' +// 暗色皮肤固定为 ink —— 是当前唯一支持的暗端皮肤。 +const DARK_SKIN = 'ink' +// 亮色皮肤的合法集合(后台 Settings 也只让站主从这里选) +const LIGHT_SKINS = ['paper', 'sage', 'rose'] + +// 客户端显示模式 —— 持久化在 localStorage +const MODE_KEY = 'one.themeMode' +const VALID_MODES = ['light', 'dark', 'auto'] + +function readMode() { + if (typeof localStorage === 'undefined') return 'auto' + try { + const v = localStorage.getItem(MODE_KEY) + return VALID_MODES.includes(v) ? v : 'auto' + } catch (_) { + return 'auto' + } +} + +function systemPrefersDark() { + if (typeof window === 'undefined' || !window.matchMedia) return false + return window.matchMedia('(prefers-color-scheme: dark)').matches +} + +// 模式 → 实际 data-theme 值 +// +// light / auto 亮端 → site.light_skin_id(站主在后台选的"基础亮色皮肤") +// dark → ink(强制暗色) +// +// 后台的"基础亮色皮肤"是访客在任何「浅色偏好」下都会看到的; +// 这样访客无论是手动锁浅色、还是系统亮色跟随自动,都能感受到站主定下的气质。 +// 暗色端固定为 ink,等后续真要加暗色变体再扩字段。 +function effectiveTheme(mode, lightSkin) { + if (mode === 'dark') return 'ink' + if (mode === 'auto') { + // auto 模式:系统暗 → ink;系统亮 → 站主选的亮色皮肤 + return systemPrefersDark() ? 'ink' : (lightSkin || 'paper') + } + // light → 站主选的亮色皮肤(让访客也能感受到站主的气质) + return lightSkin || 'paper' +} + +function isValidLightSkin(id) { + return LIGHT_SKINS.includes(id) +} + export const site = reactive({ site_title: 'ONE · 一个博客', site_desc: '长文与短文,同一种节奏。', @@ -9,21 +55,71 @@ export const site = reactive({ footer_note: '© ONE · 一个博客', icp: '', posts_per_page: 10, + light_skin_id: 'paper', + // theme_id 是老字段,保留读取兼容用;新代码不要再写。 + theme_id: 'paper', + themeMode: readMode(), loaded: false }) +// data-theme → 前台皮肤(paper / sage / rose / ink) +// data-admin-theme → 后台明暗(light / dark),跟 site.themeMode 同步 +function applyTheme() { + const isDark = site.themeMode === 'dark' || (site.themeMode === 'auto' && systemPrefersDark()) + document.documentElement.setAttribute('data-theme', effectiveTheme(site.themeMode, site.light_skin_id)) + document.documentElement.setAttribute('data-admin-theme', isDark ? 'dark' : 'light') +} +applyTheme() + +// auto 模式下系统切到夜间时实时跟随 +if (typeof window !== 'undefined' && window.matchMedia) { + const mq = window.matchMedia('(prefers-color-scheme: dark)') + const onSystemChange = () => { + if (site.themeMode === 'auto') applyTheme() + } + if (mq.addEventListener) mq.addEventListener('change', onSystemChange) + else if (mq.addListener) mq.addListener(onSystemChange) +} + export async function loadSite() { try { - Object.assign(site, await publicApi.site()) + const data = await publicApi.site() + Object.assign(site, data) + // 防御性:服务端返回的 light_skin_id 必须是白名单之一,否则兜底 paper + if (!isValidLightSkin(site.light_skin_id)) site.light_skin_id = 'paper' site.loaded = true } catch (e) { - // 站点还没起来时用默认值,不阻塞首屏 site.loaded = false } + applyTheme() return site } +// 访客切模式 → 落 localStorage + 立刻应用 +watch( + () => site.themeMode, + () => { + try { + localStorage.setItem(MODE_KEY, site.themeMode) + } catch (_) { + /* private mode 等场景静默 */ + } + applyTheme() + } +) + +// 站主改 light_skin_id → 仅在 auto 模式下立即可见; +// 否则尊重访客的显式选择。 +watch(() => site.light_skin_id, () => { + if (site.themeMode === 'auto') applyTheme() +}) + export function applyDocTitle(sub) { const base = site.site_title || 'ONE' document.title = sub ? `${sub} · ${base}` : base } + +export const SKIN_CONSTANTS = Object.freeze({ + DARK_SKIN, + LIGHT_SKINS: [...LIGHT_SKINS] +}) \ No newline at end of file diff --git a/frontend/src/styles.css b/frontend/src/styles.css index afc7625..2093117 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -36,6 +36,96 @@ --gutter: 28px; } +/* ---------- 主题:前台视图响应 site.theme_id ---------- + 后台自己的 token(--admin-*)保持米白纸感不变。 + 切换 [data-theme] 即可换肤;默认 paper 不写在选择器里。 */ + +:root[data-theme='ink'] { + --paper: #1f1d1a; + --card: #25221e; + --paper-sunken: #2b2722; + --ink: #ece5d2; + --ink-soft: #c3b58f; + --muted: #8c8270; + --faint: #6a6155; + --line: #3a352e; + --line-soft: #2f2b25; + --accent: #c3a972; + --accent-soft: rgba(195, 169, 114, 0.14); + --accent-line: rgba(195, 169, 114, 0.4); +} + +:root[data-theme='sage'] { + --paper: #eef0e6; + --card: #f6f7ef; + --paper-sunken: #e2e6d6; + --ink: #2a3127; + --ink-soft: #475042; + --muted: #6f7a68; + --faint: #94a08d; + --line: #d6dcc9; + --line-soft: #e6e9d9; + --accent: #5f7b5c; + --accent-soft: rgba(95, 123, 92, 0.12); + --accent-line: rgba(95, 123, 92, 0.38); +} + +:root[data-theme='rose'] { + --paper: #f7eee6; + --card: #fdf5ee; + --paper-sunken: #ecdfd3; + --ink: #3a2922; + --ink-soft: #5e463d; + --muted: #9a7c6f; + --faint: #b89c91; + --line: #ead9cb; + --line-soft: #f0e2d5; + --accent: #a55a4a; + --accent-soft: rgba(165, 90, 74, 0.1); + --accent-line: rgba(165, 90, 74, 0.38); +} + +/* ---------- 后台主题:跟随访客 mode 切换明暗 ---------- + data-admin-theme 与 data-theme 是两套轴:data-theme 控制前台皮肤, + data-admin-theme 只切后台的明/暗。后台暗色用深棕墨感,跟前台 ink 协调。 */ + +:root[data-admin-theme='dark'] { + --admin-bg: #1a1815; + --admin-paper: var(--admin-bg); + --admin-card: #25221e; + --admin-paper-sunken: #2f2b25; + --admin-ink: #ece5d2; + --admin-ink-soft: #c3b58f; + --admin-muted: #9a8f7e; + --admin-faint: #6e6557; + --admin-line: #3a352e; + --admin-line-soft: #2f2b25; + --admin-accent: #c3a972; + --admin-accent-soft: rgba(195, 169, 114, 0.16); + --admin-accent-line: rgba(195, 169, 114, 0.42); + --admin-on-accent: #1a1815; + --admin-danger: #d68d75; + --admin-shadow: 0 4px 14px rgba(0, 0, 0, 0.4); + --admin-overlay: rgba(0, 0, 0, 0.6); +} + +/* 主题切换时让背景渐变,避免硬切 */ +body { + transition: background-color 0.2s ease, color 0.2s ease; +} + +a, +.btn, +.tag-chip, +.pill, +.editor-mode button, +.chip, +input.input, +input.select, +textarea.textarea { + transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease; +} + * { box-sizing: border-box; } @@ -182,8 +272,9 @@ h4 { } .pill:hover { - background: #356f88; - color: #fff; + background: var(--accent-soft); + color: var(--accent); + border-color: var(--accent-line); } .pill-ghost { @@ -250,8 +341,9 @@ h4 { } .btn-primary:hover { - background: #356f88; - color: #fff; + background: var(--accent-soft); + color: var(--accent); + border-color: var(--accent-line); } .btn-danger:hover { @@ -441,3 +533,1701 @@ h4 { color: var(--muted); font-size: 14px; } + +/* ---------- 主题:前台视图响应 site.theme_id ---------- + 后台自己的 token(--admin-*)保持米白纸感不变。 + 切换 [data-theme] 即可换肤;默认 paper 不写在选择器里。 */ + +:root[data-theme='ink'] { + --paper: #1f1d1a; + --card: #25221e; + --paper-sunken: #2b2722; + --ink: #ece5d2; + --ink-soft: #c3b58f; + --muted: #8c8270; + --faint: #6a6155; + --line: #3a352e; + --line-soft: #2f2b25; + --accent: #c3a972; + --accent-soft: rgba(195, 169, 114, 0.14); + --accent-line: rgba(195, 169, 114, 0.4); +} + +:root[data-theme='sage'] { + --paper: #eef0e6; + --card: #f6f7ef; + --paper-sunken: #e2e6d6; + --ink: #2a3127; + --ink-soft: #475042; + --muted: #6f7a68; + --faint: #94a08d; + --line: #d6dcc9; + --line-soft: #e6e9d9; + --accent: #5f7b5c; + --accent-soft: rgba(95, 123, 92, 0.12); + --accent-line: rgba(95, 123, 92, 0.38); +} + +:root[data-theme='rose'] { + --paper: #f7eee6; + --card: #fdf5ee; + --paper-sunken: #ecdfd3; + --ink: #3a2922; + --ink-soft: #5e463d; + --muted: #9a7c6f; + --faint: #b89c91; + --line: #ead9cb; + --line-soft: #f0e2d5; + --accent: #a55a4a; + --accent-soft: rgba(165, 90, 74, 0.1); + --accent-line: rgba(165, 90, 74, 0.38); +} + +/* ---------- 后台主题:跟随访客 mode 切换明暗 ---------- + data-admin-theme 与 data-theme 是两套轴:data-theme 控制前台皮肤, + data-admin-theme 只切后台的明/暗。后台暗色用深棕墨感,跟前台 ink 协调。 */ + +:root[data-admin-theme='dark'] { + --admin-bg: #1a1815; + --admin-paper: var(--admin-bg); + --admin-card: #25221e; + --admin-paper-sunken: #2f2b25; + --admin-ink: #ece5d2; + --admin-ink-soft: #c3b58f; + --admin-muted: #9a8f7e; + --admin-faint: #6e6557; + --admin-line: #3a352e; + --admin-line-soft: #2f2b25; + --admin-accent: #c3a972; + --admin-accent-soft: rgba(195, 169, 114, 0.16); + --admin-accent-line: rgba(195, 169, 114, 0.42); + --admin-on-accent: #1a1815; + --admin-danger: #d68d75; + --admin-shadow: 0 4px 14px rgba(0, 0, 0, 0.4); + --admin-overlay: rgba(0, 0, 0, 0.6); +} + +/* 主题切换时让背景渐变,避免硬切 */ +body { + transition: background-color 0.2s ease, color 0.2s ease; +} + +a, +.btn, +.tag-chip, +.pill, +.editor-mode button, +.chip, +input.input, +input.select, +textarea.textarea { + transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; +} + +body { + background: var(--paper); + color: var(--ink); + font-family: var(--sans); + font-size: 16px; + line-height: var(--lh); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + /* 移动端不留横向滚动 */ + overflow-x: hidden; +} + +a { + color: inherit; + text-decoration: none; +} + +a:hover { + color: var(--accent); +} + +img { + max-width: 100%; +} + +button, +input, +textarea, +select { + font-family: inherit; + font-size: inherit; + line-height: inherit; + color: inherit; +} + +h1, +h2, +h3, +h4 { + font-family: var(--serif); + font-weight: 600; + line-height: 1.4; + margin: 0; + letter-spacing: 0.01em; +} + +::selection { + background: rgba(61, 127, 156, 0.18); +} + +/* ---------- 通用容器 ---------- */ + +.shell { + max-width: 1238px; + margin: 0 auto; + padding: 0 20px; +} + +.layout { + display: grid; + grid-template-columns: var(--col-left) minmax(0, var(--col-main)) var(--col-right); + gap: var(--gutter); + align-items: start; +} + +@media (max-width: 1080px) { + .layout { + grid-template-columns: var(--col-left) minmax(0, 1fr); + } + .layout > .rail-right { + display: none; + } +} + +@media (max-width: 780px) { + .layout { + grid-template-columns: minmax(0, 1fr); + } + .layout > .rail-left { + display: none; + } +} + +/* ---------- 小组件 ---------- */ + +.eyebrow { + font-size: 12px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--muted); +} + +.divider { + height: 1px; + background: var(--line); + border: 0; + margin: 0; +} + +.muted { + color: var(--muted); +} + +.mono { + font-family: var(--mono); + font-size: 13px; +} + +.tag-chip { + display: inline-block; + padding: 1px 8px; + border: 1px solid var(--line); + border-radius: 999px; + font-size: 12px; + line-height: 1.8; + color: var(--ink-soft); + background: transparent; +} + +.tag-chip:hover { + border-color: var(--accent-line); + color: var(--accent); +} + +.pill { + display: inline-block; + padding: 7px 18px; + border-radius: 999px; + background: var(--accent); + color: #fff; + font-size: 14px; + border: 1px solid var(--accent); + cursor: pointer; +} + +.pill:hover { + background: var(--accent-soft); + color: var(--accent); + border-color: var(--accent-line); +} + +.pill-ghost { + background: transparent; + color: var(--accent); +} + +.pill-ghost:hover { + background: var(--accent-soft); + color: var(--accent); +} + +/* ---------- 表单 ---------- */ + +.field { + margin-bottom: 18px; +} + +.field > label { + display: block; + font-size: 13px; + color: var(--muted); + margin-bottom: 6px; +} + +.input, +.textarea, +.select { + width: 100%; + padding: 9px 12px; + background: var(--card); + border: 1px solid var(--line); + border-radius: 3px; + outline: none; +} + +.input:focus, +.textarea:focus, +.select:focus { + border-color: var(--accent-line); +} + +.textarea { + resize: vertical; +} + +.btn { + padding: 8px 18px; + border: 1px solid var(--line); + background: var(--card); + border-radius: 3px; + cursor: pointer; +} + +.btn:hover { + border-color: var(--accent-line); + color: var(--accent); +} + +.btn-primary { + background: var(--accent); + border-color: var(--accent); + color: #fff; +} + +.btn-primary:hover { + background: var(--accent-soft); + color: var(--accent); + border-color: var(--accent-line); +} + +.btn-danger:hover { + border-color: #b4553f; + color: #b4553f; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* ---------- 正文排版(长文:首字下沉 + 1.95 行高) ---------- */ + +.prose { + font-size: 16.5px; + line-height: var(--lh); + color: var(--ink); +} + +.prose > :first-child { + margin-top: 22px; +} + +.prose p { + margin: 0 0 1.1em; +} + +/* 首段首字下沉 */ +.prose > p:first-of-type::first-letter { + float: left; + font-family: var(--serif); + font-size: 52px; + line-height: 1; + margin: 6px 10px 0 0; + color: var(--accent); +} + +.prose h1, +.prose h2, +.prose h3, +.prose h4 { + font-family: var(--serif); + color: var(--ink); + margin: 1.8em 0 0.6em; +} + +.prose h1 { + font-size: 26px; +} + +.prose h2 { + font-size: 22px; + padding-bottom: 6px; + border-bottom: 1px solid var(--line); +} + +.prose h3 { + font-size: 18px; +} + +.prose h3::after { + content: ''; + display: block; + width: 28px; + height: 2px; + margin-top: 8px; + background: var(--accent); +} + +.prose h4 { + font-size: 16.5px; +} + +/* 引言用衬线,和正文区分开 */ +.prose blockquote { + margin: 1.4em 0; + padding: 2px 0 2px 18px; + border-left: 2px solid var(--accent-line); + font-family: var(--serif); + color: var(--ink-soft); +} + +.prose blockquote p:last-child { + margin-bottom: 0; +} + +.prose ul, +.prose ol { + margin: 0 0 1.1em; + padding-left: 1.4em; +} + +.prose li { + margin-bottom: 0.35em; +} + +.prose a { + color: var(--accent); + border-bottom: 1px solid var(--accent-line); +} + +.prose a:hover { + border-bottom-color: var(--accent); +} + +.prose code { + font-family: var(--mono); + font-size: 13.5px; + background: var(--paper-sunken); + padding: 1px 5px; + border-radius: 3px; +} + +.prose pre { + background: var(--paper-sunken); + border: 1px solid var(--line-soft); + border-radius: 4px; + padding: 14px 16px; + overflow-x: auto; + line-height: 1.7; +} + +.prose pre code { + background: none; + padding: 0; + font-size: 13.5px; +} + +.prose img { + border-radius: 3px; + display: block; +} + +.prose hr { + height: 1px; + border: 0; + background: var(--line); + margin: 2em 0; +} + +.prose table { + width: 100%; + border-collapse: collapse; + margin: 1.4em 0; + font-size: 15px; +} + +.prose th, +.prose td { + border: 1px solid var(--line); + padding: 7px 10px; + text-align: left; +} + +.prose th { + background: var(--paper-sunken); + font-weight: 600; +} + +/* 短文:放大正文,不做首字下沉 */ +.prose-short { + font-size: 20px; + line-height: 1.9; +} + +.prose-short > p:first-of-type::first-letter { + float: none; + font-size: inherit; + line-height: inherit; + margin: 0; + color: inherit; +} + +/* ---------- 空状态 / 加载 ---------- */ + +.empty { + padding: 48px 0; + text-align: center; + color: var(--muted); + font-size: 14px; +} + +.loading { + padding: 48px 0; + text-align: center; + color: var(--muted); + font-size: 14px; +} + +/* ============================================================ + ADMIN — 后台管理界面(侧边栏布局 + 卡片化) + ============================================================ */ + +:root { + --admin-sidebar: 224px; + --admin-topbar-h: 56px; + --admin-bg: #f6f2ea; + --admin-paper: var(--admin-bg); + --admin-card: #fffdf8; + --admin-paper-sunken: #f0ebde; + --admin-ink: #33302b; + --admin-ink-soft: #5c564d; + --admin-muted: #8a8378; + --admin-faint: #a9a297; + --admin-line: #ebe4d5; + --admin-line-soft: #f3eedf; + --admin-accent: #2f6f88; + --admin-accent-soft: rgba(47, 111, 136, 0.1); + --admin-accent-line: rgba(47, 111, 136, 0.35); + --admin-on-accent: #ffffff; + --admin-danger: #b4553f; + --admin-shadow: 0 4px 14px rgba(0, 0, 0, 0.08); + --admin-overlay: rgba(0, 0, 0, 0.4); +} + +.admin-shell { + display: grid; + grid-template-columns: var(--admin-sidebar) minmax(0, 1fr); + min-height: 100vh; + background: var(--admin-bg); +} + +/* ---------- sidebar ---------- */ + +.admin-side { + background: var(--admin-card); + border-right: 1px solid var(--admin-line); + display: flex; + flex-direction: column; + position: sticky; + top: 0; + height: 100vh; +} + +.admin-side .brand { + padding: 18px 20px 16px; + border-bottom: 1px solid var(--admin-line); + display: flex; + align-items: baseline; + gap: 6px; +} + +.admin-side .brand .name { + font-family: var(--serif); + font-size: 18px; + font-weight: 600; + color: var(--admin-ink); +} + +.admin-side .brand .label { + font-size: 12px; + color: var(--admin-muted); +} + +.admin-side .nav { + padding: 14px 10px; + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; + overflow-y: auto; +} + +.admin-side .group { + font-size: 11px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--admin-muted); + padding: 14px 12px 6px; +} + +.admin-side .item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: 6px; + font-size: 14px; + color: var(--admin-ink-soft); + border: 1px solid transparent; + cursor: pointer; +} + +.admin-side .item:hover { + background: var(--admin-paper-sunken); + color: var(--admin-ink); +} + +.admin-side .item.router-link-exact-active { + background: var(--admin-accent-soft); + color: var(--admin-accent); + font-weight: 500; +} + +.admin-side .item .ic { + width: 18px; + display: inline-flex; + justify-content: center; + font-size: 16px; + line-height: 1; +} + +.admin-side .item .badge { + margin-left: auto; + font-size: 11px; + color: var(--admin-muted); + background: var(--admin-paper-sunken); + padding: 1px 7px; + border-radius: 999px; + min-width: 22px; + text-align: center; +} + +.admin-side .footer { + border-top: 1px solid var(--admin-line); + padding: 14px 18px; + font-size: 12.5px; + color: var(--admin-muted); +} + +.admin-side .footer .who { + color: var(--admin-ink-soft); + font-weight: 500; +} + +.admin-side .footer .leave { + display: block; + margin-top: 6px; + color: var(--admin-muted); + background: none; + border: 0; + padding: 0; + cursor: pointer; + font-size: 12px; +} + +.admin-side .footer .leave:hover { + color: var(--admin-accent); +} + +/* ---------- topbar ---------- */ + +.admin-main { + display: flex; + flex-direction: column; + min-width: 0; +} + +.admin-topbar { + height: var(--admin-topbar-h); + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 0 28px; + background: var(--admin-card); + border-bottom: 1px solid var(--admin-line); + position: sticky; + top: 0; + z-index: 5; +} + +.admin-topbar .crumbs { + font-size: 14px; + color: var(--admin-ink-soft); +} + +.admin-topbar .crumbs strong { + color: var(--admin-ink); + font-weight: 600; +} + +.admin-topbar .right { + display: flex; + align-items: center; + gap: 10px; +} + +.admin-topbar .cmd-hint { + font-size: 12px; + color: var(--admin-muted); + border: 1px solid var(--admin-line); + padding: 4px 10px; + border-radius: 6px; + cursor: pointer; + background: var(--admin-paper); +} + +.admin-topbar .cmd-hint kbd { + font-family: var(--mono); + font-size: 11px; + border: 1px solid var(--admin-line); + padding: 1px 5px; + border-radius: 3px; + background: var(--admin-card); +} + +.admin-content { + padding: 26px 28px 80px; + max-width: 1180px; + width: 100%; + margin: 0 auto; +} + +@media (max-width: 860px) { + .admin-shell { + grid-template-columns: 1fr; + } + .admin-side { + position: static; + height: auto; + border-right: 0; + border-bottom: 1px solid var(--admin-line); + } + .admin-side .nav { + flex-direction: row; + flex-wrap: wrap; + padding: 8px; + } + .admin-side .group { + display: none; + } + .admin-side .footer { + display: none; + } + .admin-content { + padding: 20px 16px 60px; + } +} + +/* ---------- card / panel ---------- */ + +.panel { + background: var(--admin-card); + border: 1px solid var(--admin-line); + border-radius: 8px; + padding: 18px 20px; +} + +.panel + .panel { + margin-top: 16px; +} + +.panel-title { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 14px; +} + +.panel-title h2 { + font-family: var(--sans); + font-size: 14px; + font-weight: 600; + color: var(--admin-ink); +} + +.panel-title .meta { + font-size: 12px; + color: var(--admin-muted); +} + +/* ---------- dashboard stat cards ---------- */ + +.stat-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 14px; + margin-bottom: 24px; +} + +.stat { + background: var(--admin-card); + border: 1px solid var(--admin-line); + border-radius: 8px; + padding: 16px 18px; + display: flex; + flex-direction: column; + gap: 6px; + position: relative; + overflow: hidden; +} + +.stat .label { + font-size: 12px; + color: var(--admin-muted); + letter-spacing: 0.06em; +} + +.stat .value { + font-family: var(--serif); + font-size: 28px; + font-weight: 600; + color: var(--admin-ink); + line-height: 1; +} + +.stat .hint { + font-size: 12px; + color: var(--admin-muted); +} + +.stat.accent { + background: var(--admin-accent); + border-color: var(--admin-accent); + color: var(--admin-on-accent); +} + +.stat.accent .label, +.stat.accent .hint { + color: rgba(255, 255, 255, 0.78); +} + +.stat.accent .value { + color: var(--admin-on-accent); +} + +/* ---------- dashboard chart ---------- */ + +.dash-chart { + display: flex; + align-items: flex-end; + gap: 6px; + height: 140px; + padding: 14px 4px 6px; + border-bottom: 1px solid var(--admin-line); + margin-bottom: 8px; +} + +.dash-chart .bar { + flex: 1; + background: var(--admin-accent-soft); + border-top: 2px solid var(--admin-accent); + border-radius: 3px 3px 0 0; + position: relative; + min-height: 2px; + display: flex; + align-items: flex-end; + justify-content: center; +} + +.dash-chart .bar:hover { + background: var(--admin-accent); +} + +.dash-chart .bar .v { + position: absolute; + top: -18px; + font-size: 11px; + color: var(--admin-muted); +} + +.dash-chart .labels { + display: flex; + gap: 6px; +} + +.dash-chart .labels span { + flex: 1; + font-size: 10px; + color: var(--admin-muted); + text-align: center; +} + +/* ---------- recent list ---------- */ + +.recent-list { + display: flex; + flex-direction: column; +} + +.recent-item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 4px; + border-bottom: 1px solid var(--admin-line); +} + +.recent-item:last-child { + border-bottom: 0; +} + +.recent-item .title { + flex: 1; + font-size: 14px; + color: var(--admin-ink); + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.recent-item .title:hover { + color: var(--admin-accent); +} + +.recent-item .meta { + font-size: 12px; + color: var(--admin-muted); + white-space: nowrap; +} + +/* ---------- chip ---------- */ + +.chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 10px; + border: 1px solid var(--admin-line); + border-radius: 999px; + font-size: 12px; + color: var(--admin-ink-soft); + background: var(--admin-paper); + cursor: pointer; + transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease; +} + +.chip:hover { + border-color: var(--admin-accent); + color: var(--admin-accent); +} + +.chip.on { + background: var(--admin-accent); + border-color: var(--admin-accent); + color: var(--admin-on-accent); +} + +.chip .x { + border: 0; + background: none; + cursor: pointer; + color: inherit; + padding: 0 0 0 2px; + font-size: 14px; + line-height: 1; +} + +.chip-static { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 10px; + border: 1px solid var(--admin-line); + border-radius: 999px; + font-size: 12px; + color: var(--admin-ink-soft); + background: transparent; +} + +/* tag with color dot */ +.tag-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; +} + +/* ---------- post list (cards) ---------- */ + +.posts-toolbar { + display: flex; + gap: 8px; + flex-wrap: wrap; + align-items: center; + margin-bottom: 14px; +} + +.posts-toolbar .grow { + flex: 1; + min-width: 180px; +} + +.post-card { + display: grid; + grid-template-columns: 96px minmax(0, 1fr) 110px 90px; + gap: 16px; + align-items: center; + padding: 16px 18px; + background: var(--admin-card); + border: 1px solid var(--admin-line); + border-radius: 8px; + margin-bottom: 10px; + cursor: pointer; + transition: border-color 0.15s; +} + +.post-card:hover { + border-color: var(--admin-accent); +} + +.post-card.selected { + border-color: var(--admin-accent); + background: var(--admin-accent-soft); +} + +.post-card .cover { + width: 96px; + height: 64px; + background: var(--admin-paper-sunken); + border-radius: 4px; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +} + +.post-card .cover img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.post-card .cover .ph { + font-size: 11px; + color: var(--admin-muted); + letter-spacing: 0.1em; +} + +.post-card .body { + min-width: 0; +} + +.post-card .title { + font-family: var(--serif); + font-size: 16px; + color: var(--admin-ink); + font-weight: 600; + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.post-card .summary { + font-size: 13px; + color: var(--admin-muted); + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + margin-bottom: 6px; +} + +.post-card .meta { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; + font-size: 11.5px; + color: var(--admin-muted); +} + +.post-card .meta .dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--admin-faint); + display: inline-block; + margin-right: 4px; +} + +.post-card .meta .dot.published { + background: var(--admin-accent); +} + +.post-card .stats { + font-size: 12px; + color: var(--admin-muted); + text-align: center; + font-family: var(--mono); +} + +.post-card .stats div { + line-height: 1.4; +} + +.post-card .actions { + display: flex; + flex-direction: column; + gap: 6px; + align-items: stretch; +} + +.post-card .actions button, +.post-card .actions a { + font-size: 12.5px; + padding: 4px 0; + background: none; + border: 0; + color: var(--admin-accent); + cursor: pointer; + text-align: right; + text-decoration: none; +} + +.post-card .actions .danger { + color: #b4553f; +} + +.post-card .check { + width: 18px; + height: 18px; + accent-color: var(--admin-accent); + cursor: pointer; +} + +.post-card.has-check { + grid-template-columns: 24px 96px minmax(0, 1fr) 110px 90px; +} + +@media (max-width: 760px) { + .post-card { + grid-template-columns: 64px minmax(0, 1fr); + } + .post-card .cover { + width: 64px; + height: 64px; + } + .post-card .stats, + .post-card .actions { + display: none; + } +} + +/* bulk action bar */ +.bulk-bar { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + background: var(--admin-accent); + color: var(--admin-on-accent); + border-radius: 8px; + margin-bottom: 12px; + font-size: 13px; +} + +.bulk-bar button { + background: rgba(255, 255, 255, 0.18); + color: var(--admin-on-accent); + border: 1px solid rgba(255, 255, 255, 0.3); + padding: 4px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 13px; +} + +.bulk-bar button:hover { + background: rgba(255, 255, 255, 0.28); +} + +.bulk-bar .grow { + flex: 1; +} + +.bulk-bar .x { + background: none; + border: 0; + color: var(--admin-on-accent); + font-size: 16px; + cursor: pointer; +} + +/* ---------- editor enhancements ---------- */ + +.editor-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) 280px; + gap: 20px; + align-items: start; +} + +@media (max-width: 920px) { + .editor-grid { + grid-template-columns: minmax(0, 1fr); + } +} + +.cover-block { + margin-bottom: 16px; + border: 1px dashed var(--admin-line); + border-radius: 6px; + overflow: hidden; + background: var(--admin-card); +} + +.cover-block.has { + border-style: solid; +} + +.cover-block .preview { + position: relative; + height: 160px; + background: var(--admin-paper-sunken); + background-size: cover; + background-position: center; +} + +.cover-block .preview .remove { + position: absolute; + top: 8px; + right: 8px; + background: rgba(0, 0, 0, 0.6); + color: #fff; + border: 0; + border-radius: 4px; + padding: 4px 10px; + cursor: pointer; + font-size: 12px; +} + +.cover-block .input-row { + display: flex; + gap: 6px; + padding: 10px 12px; +} + +.cover-block .input-row input { + flex: 1; + border: 0; + background: transparent; + outline: none; + font-size: 13px; + color: var(--admin-ink-soft); +} + +.cover-block .input-row button { + border: 0; + background: var(--admin-accent); + color: var(--admin-on-accent); + padding: 4px 12px; + border-radius: 3px; + cursor: pointer; + font-size: 12px; +} + +.cover-block .placeholder { + padding: 26px 18px; + text-align: center; + font-size: 13px; + color: var(--admin-muted); +} + +.editor-mode { + display: inline-flex; + border: 1px solid var(--admin-line); + border-radius: 6px; + overflow: hidden; + background: var(--admin-card); +} + +.editor-mode button { + padding: 4px 12px; + font-size: 12.5px; + border: 0; + background: transparent; + cursor: pointer; + color: var(--admin-ink-soft); +} + +.editor-mode button.on { + background: var(--admin-accent); + color: var(--admin-on-accent); +} + +.editor-stats { + display: flex; + gap: 14px; + align-items: center; + font-size: 12px; + color: var(--admin-muted); + padding: 8px 4px 0; +} + +.editor-stats strong { + color: var(--admin-ink); + font-weight: 600; + font-family: var(--mono); +} + +.save-bar { + display: flex; + align-items: center; + gap: 12px; + font-size: 12.5px; + color: var(--admin-muted); + margin-bottom: 8px; +} + +.save-bar .dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--admin-faint); +} + +.save-bar.saving .dot { + background: #d39e2f; + animation: pulse 1.2s ease-in-out infinite; +} + +.save-bar.saved .dot { + background: #6c8a4f; +} + +.save-bar.dirty .dot { + background: #b4553f; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.3; + } +} + +.md-pane { + width: 100%; + min-height: 460px; + border: 1px solid var(--admin-line); + border-radius: 0 3px 3px 3px; + background: var(--admin-card); + padding: 18px 22px; + font-family: var(--mono); + font-size: 14px; + line-height: 1.7; + color: var(--admin-ink); + resize: vertical; + outline: none; +} + +.md-pane:focus { + border-color: var(--admin-accent); +} + +.outline { + position: sticky; + top: 84px; + max-height: calc(100vh - 120px); + overflow-y: auto; + border-left: 1px solid var(--admin-line); + padding: 6px 0 6px 14px; + font-size: 12.5px; +} + +.outline .ttl { + font-size: 11px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--admin-muted); + margin-bottom: 6px; +} + +.outline ul { + list-style: none; + margin: 0; + padding: 0; +} + +.outline li { + padding: 3px 0; + color: var(--admin-ink-soft); + cursor: pointer; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.outline li:hover { + color: var(--admin-accent); +} + +.outline li.h3 { + padding-left: 12px; + font-size: 12px; +} + +.outline li.active { + color: var(--admin-accent); + font-weight: 500; +} + +/* ---------- tag manager ---------- */ + +.tag-row { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border: 1px solid var(--admin-line); + border-radius: 6px; + margin-bottom: 8px; + background: var(--admin-card); +} + +.tag-row .swatch { + width: 28px; + height: 28px; + border-radius: 50%; + border: 1px solid var(--admin-line); + flex-shrink: 0; + cursor: pointer; + position: relative; + overflow: hidden; +} + +.tag-row .swatch input[type=color] { + position: absolute; + inset: 0; + opacity: 0; + cursor: pointer; +} + +.tag-row .name { + flex: 1; + font-size: 14px; + border: 0; + background: transparent; + outline: none; +} + +.tag-row .count { + font-size: 12px; + color: var(--admin-muted); + font-family: var(--mono); +} + +.tag-row .acts { + display: flex; + gap: 6px; +} + +.tag-row .acts button { + border: 0; + background: none; + font-size: 12.5px; + cursor: pointer; + color: var(--admin-accent); + padding: 2px 6px; +} + +.tag-row .acts .danger { + color: #b4553f; +} + +/* ---------- settings groups ---------- */ + +.settings-grid { + display: grid; + grid-template-columns: 200px minmax(0, 1fr); + gap: 24px; + align-items: start; +} + +@media (max-width: 760px) { + .settings-grid { + grid-template-columns: 1fr; + } +} + +.settings-tabs { + display: flex; + flex-direction: column; + gap: 2px; + position: sticky; + top: 84px; +} + +.settings-tabs button { + text-align: left; + padding: 8px 14px; + border: 0; + background: transparent; + cursor: pointer; + font-size: 14px; + color: var(--admin-ink-soft); + border-radius: 6px; + border-left: 2px solid transparent; +} + +.settings-tabs button:hover { + background: var(--admin-paper-sunken); +} + +.settings-tabs button.on { + background: var(--admin-accent-soft); + color: var(--admin-accent); + border-left-color: var(--admin-accent); + font-weight: 500; +} + +.theme-swatches { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.theme-swatches .sw { + width: 64px; + height: 64px; + border-radius: 8px; + border: 2px solid var(--admin-line); + cursor: pointer; + position: relative; + overflow: hidden; +} + +.theme-swatches .sw.on { + border-color: var(--admin-accent); +} + +.theme-swatches .sw span { + position: absolute; + bottom: 4px; + left: 4px; + font-size: 10px; + color: var(--admin-on-accent); + background: rgba(0, 0, 0, 0.4); + padding: 1px 5px; + border-radius: 3px; +} + +/* ---------- command palette ---------- */ + +.cmd-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + display: flex; + align-items: flex-start; + justify-content: center; + z-index: 100; + padding-top: 14vh; +} + +.cmd-panel { + width: 560px; + max-width: 90vw; + background: var(--admin-card); + border: 1px solid var(--admin-line); + border-radius: 10px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25); + overflow: hidden; +} + +.cmd-panel input { + width: 100%; + padding: 16px 18px; + border: 0; + border-bottom: 1px solid var(--admin-line); + outline: none; + font-size: 16px; + background: var(--admin-card); +} + +.cmd-list { + max-height: 360px; + overflow-y: auto; +} + +.cmd-list .row { + padding: 10px 18px; + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + border-bottom: 1px solid var(--admin-line); +} + +.cmd-list .row:last-child { + border-bottom: 0; +} + +.cmd-list .row.on, +.cmd-list .row:hover { + background: var(--admin-accent-soft); +} + +.cmd-list .row .label { + flex: 1; + font-size: 14px; +} + +.cmd-list .row .hint { + font-size: 12px; + color: var(--admin-muted); +} + +.cmd-list .row kbd { + font-family: var(--mono); + font-size: 11px; + border: 1px solid var(--admin-line); + padding: 1px 5px; + border-radius: 3px; +} + +.cmd-list .empty { + padding: 24px; + text-align: center; + color: var(--admin-muted); + font-size: 13px; +} + +.shortcut-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 12px; +} + +.shortcut-grid .row { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 0; + font-size: 13px; +} + +.shortcut-grid .row .lab { + flex: 1; + color: var(--admin-ink-soft); +} + +.shortcut-grid kbd { + font-family: var(--mono); + font-size: 12px; + border: 1px solid var(--admin-line); + background: var(--admin-paper); + padding: 2px 7px; + border-radius: 4px; + min-width: 22px; + text-align: center; +} + +/* ============================================================ + 全局可访问性 / 主题感知 + ============================================================ */ + +/* 跟随主题给原生控件(滚动条、表单、autofill)正确色 scheme */ +:root { + color-scheme: light; +} + +:root[data-theme='ink'], +:root[data-admin-theme='dark'] { + color-scheme: dark; +} + +/* 键盘焦点可见(仅键盘触发,不打扰鼠标用户) */ +:where(a, button, input, select, textarea, [tabindex]):focus-visible { + outline: 2px solid var(--admin-accent); + outline-offset: 2px; + border-radius: 3px; +} + +/* 前台视图焦点用前台强调色 */ +body:not(.admin-shell-active) :where(a, button, input, select, textarea, [tabindex]):focus-visible { + outline-color: var(--accent); +} + +/* 编辑器内容区不要画焦点环(Milkdown 内部已经有) */ +.crepe-host :focus-visible { + outline: none !important; +} + +/* 用户开了「减少动效」时全部动画/过渡降到接近 0 */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + body { + transition: none !important; + } +} + +/* Modal / overlay 防止滚动穿透到下层 */ +.cmd-overlay { + overscroll-behavior: contain; +} diff --git a/frontend/src/utils.js b/frontend/src/utils.js index d67c66d..6f38bfb 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.js @@ -11,23 +11,26 @@ export function renderMarkdown(text) { // 编辑器按 `md.render(...)` 的用法调用 export const md = { render: renderMarkdown } +// 用 Intl.DateTimeFormat — locale 感知,未来要 i18n 只换 locale 即可 +const longDateFmt = new Intl.DateTimeFormat('zh-CN', { + year: 'numeric', + month: 'long', + day: 'numeric' +}) +const shortDateFmt = new Intl.DateTimeFormat('sv-SE') // YYYY-MM-DD + export function formatDate(iso) { if (!iso) return '' const d = new Date(iso) if (Number.isNaN(d.getTime())) return iso - const y = d.getFullYear() - const m = String(d.getMonth() + 1).padStart(2, '0') - const day = String(d.getDate()).padStart(2, '0') - return `${y} 年 ${m} 月 ${day} 日` + return longDateFmt.format(d) } export function formatDateShort(iso) { if (!iso) return '' const d = new Date(iso) if (Number.isNaN(d.getTime())) return iso - return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String( - d.getDate() - ).padStart(2, '0')}` + return shortDateFmt.format(d) } // 时间线上的相对时间:三天内用「几小时前」,同年省略年份 diff --git a/frontend/src/views/AboutView.vue b/frontend/src/views/AboutView.vue index 4d40079..ff4f390 100644 --- a/frontend/src/views/AboutView.vue +++ b/frontend/src/views/AboutView.vue @@ -51,7 +51,7 @@ applyDocTitle('关于') position: sticky; top: 0; z-index: 10; - background: rgba(250, 247, 241, 0.94); + background-color: var(--paper); backdrop-filter: blur(6px); border-bottom: 1px solid var(--line); padding: 14px 16px; diff --git a/frontend/src/views/ArchiveView.vue b/frontend/src/views/ArchiveView.vue index aebf54d..e2504e3 100644 --- a/frontend/src/views/ArchiveView.vue +++ b/frontend/src/views/ArchiveView.vue @@ -80,7 +80,7 @@ applyDocTitle('归档') position: sticky; top: 0; z-index: 10; - background: rgba(250, 247, 241, 0.94); + background-color: var(--paper); backdrop-filter: blur(6px); border-bottom: 1px solid var(--line); padding: 14px 16px; diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index d350769..9e7cb63 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -135,29 +135,30 @@ applyDocTitle('') position: sticky; top: 0; z-index: 10; - background: rgba(250, 247, 241, 0.94); + background-color: var(--paper); backdrop-filter: blur(6px); border-bottom: 1px solid var(--line); - padding: 14px 16px 0; + padding: 8px 16px 0; } .page-title { - font-size: 20px; + font-size: 17px; + font-weight: 600; } .tabs { display: flex; - gap: 4px; - margin-top: 6px; + gap: 2px; + margin-top: 4px; } .tab { position: relative; - padding: 8px 14px 10px; + padding: 5px 12px 7px; background: none; border: 0; cursor: pointer; - font-size: 14px; + font-size: 13.5px; color: var(--muted); } diff --git a/frontend/src/views/TagView.vue b/frontend/src/views/TagView.vue index 9c6900e..f6e8b29 100644 --- a/frontend/src/views/TagView.vue +++ b/frontend/src/views/TagView.vue @@ -95,7 +95,7 @@ applyDocTitle('标签') position: sticky; top: 0; z-index: 10; - background: rgba(250, 247, 241, 0.94); + background-color: var(--paper); backdrop-filter: blur(6px); border-bottom: 1px solid var(--line); padding: 14px 16px; diff --git a/frontend/src/views/TagsView.vue b/frontend/src/views/TagsView.vue index 377040e..ddba7de 100644 --- a/frontend/src/views/TagsView.vue +++ b/frontend/src/views/TagsView.vue @@ -70,7 +70,7 @@ applyDocTitle('标签') position: sticky; top: 0; z-index: 10; - background: rgba(250, 247, 241, 0.94); + background-color: var(--paper); backdrop-filter: blur(6px); border-bottom: 1px solid var(--line); padding: 14px 16px; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 1dd1f6b..8369e0f 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -8,6 +8,8 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, + "allowJs": true, + "checkJs": false, "noEmit": true, "jsx": "preserve", "strict": true, @@ -17,5 +19,5 @@ "@/*": ["./src/*"] } }, - "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"] + "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.d.ts", "src/**/*.vue"] }