assets/creator-studio/build is generated by `npm run build`, so committing it
put a rebuilt bundle in every diff. It now follows the same deploy-only rule
as assets/admin/build.
- Root .gitignore ignores build/* and node_modules/, keeping a .gitkeep so
the directory still exists in a clean checkout.
- Drops the three tracked artifacts from the index.
- The contract test no longer requires the artifacts to exist; it checks the
set is all-or-nothing, so a clean checkout and a locally built tree both
pass. This mirrors admin-build-graph-contract.
- README documents that build/ is produced on the target machine.
887 widget-system tests pass.
The cover row carried its own hidden file input and upload call, so authors
had two different image flows in one page.
- 添加封面 now opens window.JioSubmitMedia with type=image, the same dialog
the toolbar's 图片 button uses (mine / upload / external link).
- The picked image is stored as the full-size URL plus its attachment id,
matching FrontSubmitFeatured's image branch, which re-checks ownership
and sets the post thumbnail.
- Removes the now-unused uploadCover path, apiUpload/nonce imports, and the
.jio-creator__cover-input rule.
Contract added; 887 widget-system tests pass.
Classification belongs next to everything else the author decides before
publishing, and the drawer should only configure paid resources.
- PublishSettings now renders categories, tags and custom taxonomy
dimensions first, then cover, paid content and scheduling. Chips use a
stacked row layout so long category lists wrap naturally.
- PublishDrawer keeps only PayPanel: taxonomy, save/publish buttons and the
now-unused props are gone, leaving a single 完成 button.
- New PublishDialog takes over the publish action from the bottom bar, and
with it the captcha mount point, so the gate still renders somewhere the
author can see it.
- Promotes .jio-creator-primary / .jio-creator-ghost out of the dialog-only
rules (drawer and pay panel buttons had no styling), and drops the dead
.jio-creator-field / .jio-creator-cover blocks.
Contracts updated; 886 widget-system tests pass.
The card below the writing surface only offered cover and scheduling, so
paid content could only be reached through the bottom bar's 发布 button.
- The card gains a 付费内容 row (rendered only with canPay): a toggle bound
to draft.pay.enabled, a status line that reflects whether resources were
added, and a 配置价格与付费资源 link.
- Turning the toggle on opens the publish drawer and scrolls it to the pay
section, so pricing is the next thing the author sees. Entering from the
bottom bar keeps the drawer at the top.
The drawer only had a pay toggle and one bare price field, so several
options from the front-submit pay card could not be set at all.
- New PayPanel mirrors the front-submit card: cash / points segment
(points only when the site has credits enabled), price or points with
the currency suffix, and VIP prices. VIP fields render only when the
site asks for manual VIP pricing and the level is enabled, and now
submit vip1_points / vip2_points as well.
- New ResourceDialog restores "添加资源": download URL with an attachment
picker, resource note (extraction code), copy name / value, custom
button label and button style swatches. Pasting a net-disk share link
splits the URL and the extraction code.
- pay.ts sends every download display field and tags net-disk links.
- CreatorRest exposes vipManual / vip*Enabled / vip names / pointsEnabled
/ downloadClasses / currencySymbol, and the readback carries
extractCode / copyKey / copyVal / class / attachmentId.
- Fixes an existing data loss: snapshot_from_payload dropped copy_key,
copy_val, class and attachment_id, so approving a revision erased them.
Files split to stay within budget: publish-drawer 332 lines, pay-panel
343, resource-dialog 254. 886 widget-system tests pass.
Toolbar
- New ToolMenu renders menus through a portal on body, so they are no longer
clipped by the horizontally scrolling toolbar and no longer fall back to a
native <select>. Triggers reuse .jio-creator__tool-btn geometry, fixing the
heading button's mismatched height/size/position.
- Heading / list / align / quote / hidden-content are all dropdowns.
- Lists: unordered + ordered via InsertOrderedList/InsertUnorderedList.
The TOC button is removed (it also used the wrong icon).
- Adds left / center / right alignment.
- Adds hidden content: comment / login / VIP / paid, wired to the existing
[reply], [jio_hide mode="login"], [vip] and [payshow] shortcodes.
Editor content
- Quote blocks now match the live article: full .quote_q structure, four
colour variables, and an SVG-mask quote mark that does not depend on the
Font Awesome font being present inside the iframe.
- Block math is centred on a transparent background instead of a full-width
grey bar.
- Code blocks are plain <pre><code>, identical to the article page; language
and theme selection is gone and EnlighterJS handles highlighting globally.
The classic submit editor's code modal follows the same shape.
- Hidden content reuses the .tinymce-hide structure and shows a readable label
while keeping the shortcode text in the DOM for saving.
Block deletion
- Two-step delete, WPS style: the first click only selects and highlights the
block, the second click removes it. Clicking elsewhere or Esc cancels.
Contracts updated and extended; 884 widget-system tests pass.
- assemble the friends template with page header, search cover, content position, widget zones and comments
- render the three page title styles and the tabbed search cover
- restore card, category nav and submit modal markup for the friends page
- add page content style support to the page editor
- port the linked card, category nav and submit modal styles to the mobile and desktop breakpoints
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- enable the native Links manager and drop the custom friend-link option/REST path
- add per-page link settings for the friends template
- render link categories from get_bookmarks() with search and page-content options
- add a validated public link submission endpoint with logo upload, captcha, rate limiting and duplicate checks
- create pending links for review and notify administrators by email and in-app message
- add native Links admin pending count, columns and edit notes
- cover the new submission and cleanup contracts
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Point 站点 Logo / 暗色模式 Logo schema defaults (PHP + VUE) and the
Settings::logo_url() fallback at assets/img/logo.png.
- Point the footer night image default at the same file.
- Remove logo_dark.png and the now-unreferenced assets/icons/default-logo.svg
along with the private default_asset_url() helper that was its only reader.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The navbar logo carried an inline `height:36px;width:auto;max-width:{logo_width}px`
while `.navbar-logo > img` already owns `height:36px;padding:0 30px`. Under
`box-sizing:border-box` that max-width capped the border box, so the 60px of
horizontal padding stayed inside it and wide logos were squeezed against a
fixed height, breaking the intrinsic aspect ratio.
- Render the nav logo with no inline sizing; CSS alone sizes it.
- Add a `$lazy` argument to jio_get_adaptive_theme_img() so the navbar can emit
the real src up front instead of a square lazyload placeholder; defaults to
'auto' so every other caller keeps the lazy_other behaviour.
- Remove the superseded logo_width plumbing: the setting (PHP + VUE schemas),
Settings::logo_width(), jio_get_site_logo_width(), the --jio-logo-width token
and the unused has_logo context key.
- Cover the contract in the header reference test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Point the 浏览器图标 / 移动端图标 schema defaults (PHP + VUE) at
assets/img/favicon.png instead of an empty media field.
- Fall back to the same PNG in Settings::favicon_url() when no custom
favicon and no native site_icon are configured.
- Add Settings::mobile_icon_url() and emit apple-touch-icon from SEOHead;
the 移动端图标 field previously had no consumer and never reached the page.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Traced against the running site (docker theme at AKSRT-theme) with real
computed styles:
- theme_main_radius defaulted to 12, so --main-radius was 12px and every
dropdown panel, card and button was rounder than the reference, which
sets 8px. Default is now 8 in the runtime reader and both schemas.
- The public bundle loads after main.min.css. Its bare img rule replaced
the inline image baseline the menu relies on, and html{overflow-x} made
the root a clipping context that crops the fixed senior mega menu.
Reset is now scoped to content; the horizontal-overflow guard is on body.
- The senior mega menu renders a .container inside the dropdown where the
reference keeps Bootstrap's 15px gutter; the padding-free site-shell
.container rule stripped it, running the card/link grid to the edges.
Rebuilt public.css / public.min.css and the admin SPA.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The public bundle loads after main.min.css, and its bare-element rules
won on equal specificity:
- img { display: block; height: auto } replaced the inline image baseline
the menu relies on (Bootstrap img{vertical-align:middle} + main.css
img{max-height:100%}). Menu glyphs and graphic-card thumbnails were
pulled out of the inline flow, breaking row height and baseline.
Scoped the reset to content and restored the inline baseline inside
.header / .mobile-header.
- html { overflow-x: clip/hidden } made the root a clipping context,
which crops the position:fixed senior mega menu and its drop panels.
The horizontal-overflow guard now lives on body only.
Rebuilt public.css / public.min.css.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment moderation pipeline no longer calls Akismet. Removed:
- check_akismet() and the Akismet step in Moderation::process().
- akismet_* config keys from Moderation defaults and ExtensionRuntime.
- akismet decision state, the jio_moderation_akismet / akismet_result
comment meta writes, and the legacy-meta fallbacks in the comments column.
- The Akismet fieldset in both the PHP and Vue permissions schemas.
Rate limit, GeoIP, blocked keywords and AI moderation are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Renderer:
- Graphic-card images carry fit-cover so they crop into the fixed card box
instead of stretching it and pushing the title out of alignment.
- Mega-menu link icons use fa-fw (fixed glyph column) instead of the
undefined menu-item-icon class, so link titles line up.
Backend:
- Restore show_policy (login / vip / vip_2 / auth) on menu items; the field
was missing entirely, so items could not be gated by identity.
- Default submenu_type to graphic_card, badge_class to jb-red, arrangement
to swiper, size to md.
- Clamp img_scale to 20-300 to match the reference's spinner bounds.
- Drop the now-meaningless "none" submenu_type option: the senior-submenu
switch is itself the on/off control.
- Admin JS gains the align control (left/center/space-around).
Cleanup:
- Remove the unused header_actions nav menu location.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit put the channel-switching code in the wrong init block.
credits.js initializes two modals side by side — the points recharge dialog and
the withdrawal dialog — and the snippet landed in the recharge one, which has
no [data-jio-withdraw-method] element at all. Both symptoms followed from that:
methodField resolved to null so no change listener was ever bound, and the
initial sync never ran, leaving the note paragraph permanently hidden.
The block now lives inside initWithdraw(), and the stray copy is gone. The
behaviour itself was already correct — switching channels swaps the account
placeholder and shows or hides the channel note — it just never executed.
Verified in a headless browser against the real markup and stylesheet: the
placeholder starts as the first channel's hint and the note is hidden; switching
to the second channel swaps the placeholder and reveals the note as
display:block; switching back hides it again as display:none.
The contract test previously only checked that the code existed, which is why
it missed a placement bug that made the whole block dead. It now asserts
syncChannelHint appears exactly once, sits after initWithdraw(), and does not
run past the next function definition. 873 tests pass.
The payout channel was a two-item checkbox (alipay / wechat). Sites that pay
out through a bank transfer, USDT or any other route had no way to express it,
and every surface behind it — the withdrawal modal, the server-side check, the
admin review list — hard-coded those two names.
affiliate_payout_channels is now a repeater on both the PHP and Vue schemas,
using the same draggable component as the post filters and gateway config. Each
row carries a stable key, a display label, an account placeholder, an optional
note and an enabled flag. Disabled channels disappear from the front end but
stay resolvable, so historical withdrawal records still render their original
name.
PaymentRuntime::payout_channels() normalises the stored structure: it drops rows
with an empty key or label, de-duplicates keys, and falls back to the built-in
two channels when nothing is configured. enabled_payout_channels() feeds the
front end and payout_channel() resolves a single key for record display.
CommissionLedger's validation now compares against the configured, enabled keys
instead of a literal allowlist.
The withdrawal modal renders the configured list and swaps the account
placeholder and note when the channel changes; credits.js drives that. The
admin review page reads the same settings for its method labels and filter, and
the review dialog gained a "渠道说明" row so the operator sees the payout
instruction, hidden when empty.
Default channels live in SchemaPayment as plain data — the runtime imports them
rather than the reverse, because the schema layer must not depend on a runtime
settings class (doing it the other way made the schema harness fatal).
Verification: drove payout_channels() through default / custom / dirty / duplicate
inputs and confirmed enabled filtering, note lookup and disabled-channel
resolution. Added payment-withdrawal-channels-contract.test.cjs covering the
schema shape, runtime helpers, front-end wiring and admin read path. 873 tests
pass, PSR-4 audit clean, admin SPA builds.
A settings screen renders twenty-odd AppSecret and private-key fields as
type="password". Chrome reads any page carrying a password input as a login
form and fills the saved username and password into the first text box it
finds — the settings search field at the top of the panel.
Suppressing autocomplete on that one input could not fix it, because the
trigger is the password inputs elsewhere on the page. Secret fields now render
as text with CSS masking (-webkit-text-security: disc), so they still look
like ciphertext but no longer register as password inputs; the mask lifts on
focus so a freshly typed value can be checked. That covers the 21 schema-driven
fields through JioField plus the four captcha provider forms and the WeChat
template page.
The search box keeps a second layer of defence: a descriptive name and
autocomplete/autocorrect/spellcheck disabled, with the LastPass and 1Password
ignore markers set.
Verification: vite build succeeds and the emitted bundles no longer contain a
single type="password"; 980 admin/widget tests pass.
Note: assets/admin/build/ is gitignored, so the production build must be run on
deploy. -webkit-text-security is WebKit/Blink only — Firefox falls back to
plain text.
The previous commit added five direct clients (Douyin, Microsoft, Facebook,
Twitter, Apple) written against each platform's own endpoints. Maintaining
five bespoke OAuth implementations — including Apple's ES256 client secret and
Twitter's PKCE — is maintenance the bundled yurunsoft SDK does not ask for,
and none of them could be verified without production credentials.
Only providers yurunsoft/yurun-oauth-login actually implements stay on the
direct surface: GitHub, Gitee, QQ, Google, Weibo, Weixin, Baidu and Alipay.
Baidu and Alipay move into OAuthProviderBroker's existing client() switch and
normalize() map rather than living in a separate class, so there is no new
layer to reason about.
OAuthProviderClient, its harness and its contract test are deleted outright
(-809 lines). The Apple form_post handling added to handle_callback() is
reverted too — without Apple there is no POST callback to read.
The aggregate-login channel keeps its 16 platforms, matching the reference;
that list describes what the aggregation provider offers, not a local
implementation. Brand icons for those names stay for the same reason.
Verification: 869 widget/navigation tests pass, the PSR-4 audit reports 0
errors, and the catalog constant, PHP schema and Vue schema now list the same
16 aggregate platforms.
The theme shipped six direct providers while the reference carries fourteen,
and its aggregate-login channel offered three of sixteen platforms.
OAuthProviderClient now owns the providers the bundled SDK does not cover.
Douyin, Microsoft, Facebook, Twitter and Apple talk to their documented OAuth2
endpoints directly; Baidu and Alipay reuse yurunsoft, which already ships both
(Alipay needs its application private key, so provider_config learned an
extra_keys map to surface it). The broker dispatches through handles(), so the
existing start/callback routes stay provider-agnostic.
Three provider quirks needed handling rather than a plain copy:
Apple returns the callback as response_mode=form_post, and handle_callback
only read $_GET, so it now reads $_POST when the request is a POST — otherwise
every Apple login would fail with code_missing. Its client secret is built as
an ES256 JWT from Team ID / Key ID / .p8 rather than a static string.
Twitter requires PKCE. The verifier is derived from the state token, so it
travels with the existing transaction row and needs no extra storage.
Facebook hands back protocol-less avatar URLs, which are upgraded to https the
same way the OAuth avatar resolver does.
Settings gained matching toggles and credential groups on both sides — the PHP
schema and the Vue schema — and the aggregate platform list went from three to
sixteen, with CLoginClient's hard-coded allowlist replaced by the catalog
constant. Brand icons were extended so each new provider resolves to its
sprite; facebook and twitter moved to their colour sprites, and the
`twitter -> x` alias was dropped since it bypassed the new mapping.
The oauth-security contract previously asserted that Baidu and Alipay must not
appear on the OAuth surface. That was the old intent, so the assertion is
replaced with a positive one, keeping the separate check that the Alipay
payment gateway still exists.
Verification: a new harness stubs the HTTP layer and drives all five custom
clients — authorization URLs carry redirect/state (Twitter also a PKCE
challenge), identities normalize to the shared shape, Facebook's avatar is
upgraded to https, Apple's private-relay email survives, and an error payload
throws instead of yielding an empty identity. 871 widget/navigation tests pass
and the PSR-4 audit reports 0 errors.
Not covered: no live handshake happened — every provider needs real AppID and
secret pairs, and Apple/Douyin require developer-account review before the
full round trip can be exercised.
The OAuth flow wrote the provider avatar to `jio_oauth_avatar`, but nothing
ever read it — every user who signed up through GitHub/QQ saw a Gravatar hash
or the default avatar instead of their own picture. The reference seeds its
`custom_avatar` from the OAuth profile and its resolver reads it back, so both
halves were missing here.
jio_get_custom_avatar_url() now falls back to the stored OAuth avatar when no
uploaded one exists, which puts the chain at uploaded → third-party → external
service → default. The theme already swaps WordPress's get_avatar_data, so one
change covers every avatar surface.
Writes go through a single seed_avatar() guard: it no-ops when an uploaded
avatar or an existing OAuth avatar is present, matching the reference's
`if ($args['avatar'] && !$custom_avatar)`. Previously both the bind path and
the login refresh overwrote the meta unconditionally, which would have wiped a
user's own picture on their next social login.
Because the OAuth avatar now shows through once the upload is gone, both
"restore default" flows (the REST endpoint and the admin profile box) delete
it too; the admin checkbox also appears when only an OAuth avatar is set,
since $avatar_url is now non-empty in that case.
Baidu serves avatars from tb.himg.baidu.com, which 403s — rewritten to
himg.bdimg.com the way the reference does. On top of that the URL is upgraded
to https when the site runs over SSL, because the reference's scheme-relative
form still yields a plain http:// when copied verbatim, and browsers block it
as mixed content.
WeChatShare was reading `custom_avatar` — a reference-theme key this codebase
never writes — so its author branch always fell through to get_avatar_url();
it now uses the shared resolver like every other surface.
Verification: drove the resolver through five states with a stub (none /
OAuth only / both / upload only / Baidu legacy host) and confirmed the
expected URL each time. Added oauth-avatar-contract.test.cjs to pin the
resolution order, the seed guard, and the reset cleanup. 868 widget and
navigation tests pass; the PSR-4 path audit reports 0 errors.
The username and e-mail entries under the profile header rendered as plain
grey text on a solid icon. The reference makes each one a bordered pill that
copies its own value on click, drawn with the outline glyphs.
Both entries now carry `.but` plus `data-clipboard-tag` / `data-clipboard-text`
so the existing frontend clipboard runtime (assets/js/runtime/libs/clipboard.js)
picks them up and toasts "用户名已复制". Icons move from `fas` to `far`, which
is the FA5 equivalent of the reference's `fa-user-o` / `fa-envelope-o`.
Geometry follows `.author-header .user-identity > .but/.badg` exactly:
padding 2px 6px, margin 5px 6px 0 0, 12px type, 1.4 line height, and the meta
row gets the reference's 520px max width. medal.css already carried its own
approximation of this pill (2px 8px 2px 0, .3em .6em, 1.44) for the medal
entry; it now uses the same numbers so the two entries line up. The base rule
lives in layout.css rather than medal.css because the medal module is only
enqueued when badges are enabled, and the header must keep its shape either
way.
Verification: rendered the real header through a PHP stub and measured the
computed styles in the browser — padding 2px 6px, margin 5px 6px 0 0, 12px /
16.8px, 23px tall, both entries carrying the right clipboard payload. Added
user-center-header-meta-contract.test.cjs to pin the markup and geometry;
864 widget/navigation tests pass.
Each experience task row closed with a solid `fa-bolt` glyph. The reference
marks the same slot with the colour `trend-color` sprite followed by "+ N",
and uses that sprite for the 成长体系 heading as well, so the level page also
carried a stray `fa-chart-line` there.
The sprite already ships in the theme (`jiosvg-trend-color`); its path data is
byte-identical to the reference sheet. Both slots now render it, sized to
match the reference (`icon mr6 em09` for the rows, `icon mr6 em12` for the
heading). The "动态" rows keep their wording without a "+", which is how they
read before.
The check-in experience badges in src/Credits/Signin.php stay on
`fa-line-chart` — the reference uses the same monochrome glyph there.
Verification: rendered the real level page through a PHP stub — all ten task
rows emit `#icon-trend-color`, the only Font Awesome glyph left is the
"每日详情" arrow, matching the reference. Screenshotted against the real
stylesheet. A contract test pins both sprite usages and rejects a regression
to `fa-bolt` / `fa-chart-line`; 862 widget/navigation tests pass.