feat(widgets): replicate 12 reference widgets for full Zibll parity

- rewrite schemas for posts, tab_posts, oneline_posts, hot_posts,
  term_cards, term_aggregation, media_cards, combo_info, partners,
  recent_comments, posts_toc and slider_media_combo to mirror the
  modern reference modules field by field (options, defaults,
  dependencies, min/max/step, subheadings)
- port reference renderers: hot ranking with TOP badges, category
  aggregation card, graphic cover cards, partners CSS marquee,
  combo info cards with auto band palette, recent comment list
- rename 分类与标签聚合卡片 to 分类聚合卡片 and restrict it to
  categories (no tags/topics branch in this project)
- drop topics filter fields and the topics query branch; align
  orderby value domain with supported post meta
- add searchable sortable terms admin field type and spinner unit
  suffix rendering
- unify widget pagination and lazy loading on the REST client with
  the runtime-owned ias reveal trigger; remove the legacy
  post_ajax_trigger dual path
- align section-navs TOC with the reference contract (wznav ids,
  3-heading minimum, zib-widget box)
This commit is contained in:
2026-09-05 11:28:00 +08:00
parent 9dc278bd83
commit b639d57c5b
25 changed files with 1388 additions and 446 deletions
@@ -20,7 +20,7 @@ test('icon card renderers keep vertical and horizontal legacy DOM contracts sepa
assert.doesNotMatch(helper, /aksrt-icon-cover-card/);
assert.match(widgets, /'customize_icon' => \$card\['customize_icon'\] \?\? ''/);
assert.doesNotMatch(widgets, /\$card\['custom_icon'\]/);
assert.doesNotMatch(widgets, /\$card\['icon_image'\]/);
// combo_info legitimately renders icon_image cards; icon cards must not leak media-card fields.
assert.doesNotMatch(widgets, /\$card\['description'\]/);
assert.match(header, /empty\(\$card\['custom_icon'\]\)/);
});
@@ -45,7 +45,7 @@ test('paid article and sidebar widget use the Zibll purchase-card DOM', () => {
assert.match(pay, /支付金额/);
assert.match(pay, /hidden-box show/);
assert.match(widget, /'layout' => 'widget'/);
assert.match(shell, /\['post_pay', 'tab_posts'/);
assert.match(shell, /'post_pay', 'tab_posts', 'posts'/);
assert.doesNotMatch(single, /aksrt-panel pay-box pay-type-download/);
assert.doesNotMatch(single, /get_template_part\('templates\/parts\/resource-download-box'\)/);
assert.match(single, /aksrt_get_paid_box_html\(\$post\)/);
@@ -8,9 +8,10 @@ const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
test('modern widget schemas expose Zibll field names and defaults', () => {
const source = read('src/WidgetSystem/WidgetSchemas.php');
for (const field of ['mini_opt', 'paginate', 'tabs', "'id' => 'cat'", "'id' => 'topics'", 'layout_ratio', 'slide_scale', 'm_layout_type', 'm_cover_ratio', 'image_layer', 'image_select']) {
for (const field of ['mini_opt', 'paginate', 'tabs', "'id' => 'cat'", "'id' => 'zibpay_type'", 'layout_ratio', 'slide_scale', 'm_layout_type', 'm_cover_ratio', 'image_layer', 'image_select']) {
assert.match(source, new RegExp(field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
}
assert.doesNotMatch(source, /'id' => 'topics'/);
assert.match(source, /'热门推荐'.*'orderby' => 'views'/s);
assert.match(source, /'最近更新'.*'orderby' => 'modified'/s);
assert.match(source, /'猜你喜欢'.*'orderby' => 'rand'/s);
@@ -19,13 +20,16 @@ test('modern widget schemas expose Zibll field names and defaults', () => {
test('tab and slider renderers retain reference DOM contracts', () => {
const content = read('src/WidgetSystem/Renderers/ContentRenderer.php');
const slider = read('src/WidgetSystem/Renderers/SliderRenderer.php');
for (const token of ['widget-tab-post', 'posts-mini-lists', 'index-tab', 'tab-nav-theme', 'tab-pane fade', 'post_ajax_trigger', 'post_ajax_loader', 'widget-ajaxpager']) assert.match(content, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
for (const token of ['widget-tab-post', 'posts-mini-lists', 'index-tab', 'tab-nav-theme', 'tab-pane fade', 'ias-btn', 'post_ajax_loader', 'widget-ajaxpager']) assert.match(content, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
assert.doesNotMatch(content, /post_ajax_trigger/);
for (const token of ['relative zib-slider', 'new-swiper slide-widget', 'swiper-wrapper', 'swiper-slide', 'slide-layer', 'data-swiper-parallax', 'swiper-button-prev', 'swiper-pagination', 'widget-slider-graphic', 'slider-col', 'cover-col', 'widget-graphic-cover']) assert.match(slider, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
});
test('reference slider CSS includes responsive height and combo selectors', () => {
const css = read('assets/widgets/frontend.css');
for (const token of ['.new-swiper.slide-widget.scale-height', '--scale-height', '--pc-height', '--m-height', '.widget-slider-graphic', '.m-hide_slide', '.m-hide_cover', '--m-cover-ratio']) assert.match(css, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
const mainCss = read('assets/css/main.css');
for (const token of ['.new-swiper.slide-widget.scale-height', '--scale-height', '--pc-height', '--m-height', '.widget-slider-graphic', '.m-hide_slide', '.m-hide_cover']) assert.match(css, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
for (const token of ['--m-cover-ratio', '--cover-ratio', '--layout-ratio']) assert.match(mainCss + css, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
});
test('icon card widgets expose Zibll vertical and horizontal contracts', () => {
@@ -71,9 +75,9 @@ test('vertical notice supports only custom and article sources with Zibll swiper
assert.match(schema, new RegExp(field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
}
assert.match(schema, /'options' => \['' => '自定义', 'posts' => '文章'\]/);
assert.doesNotMatch(schema, /forums_opts|notice_forum_fields|zibpay_type|zibpay_price|zibpay_points_price|sales_volume/);
assert.doesNotMatch(schema, /forums_opts|notice_forum_fields|zibpay_price|zibpay_points_price|sales_volume/);
assert.doesNotMatch(renderer, /zib_bbs_get_posts_query|forums_opts/);
assert.doesNotMatch(postQuery, /zibpay_type|zibpay_price|zibpay_points_price|sales_volume/);
assert.doesNotMatch(postQuery, /zibpay_price|zibpay_points_price|sales_volume/);
assert.doesNotMatch(form + controller + adminJs, /aksrt_widget_remote_options|aksrt-widget-remote-select|aksrt-widget-remote-picker/);
for (const token of ['swiper-bulletin mb20 ', 'notice-slide', 'new-swiper', 'data-direction="vertical"', 'data-loop="true"', 'data-autoplay="1"', 'bulletin-icon flex jc shrink0']) {
assert.match(renderer, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));