article-content-renderer-vue2 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,9 @@
6
6
 
7
7
  ## 特性
8
8
 
9
- - 支持 Article Content Protocol v1 的全部节点和 marks
9
+ - 支持 Article Content Protocol v1 + Extensions 的全部节点和 marks,兼容旧文档。
10
+ - 支持资源问题快照、宿主控制的后文解锁、实例内锚点定位和取消待跳转任务。
11
+ - 支持文字颜色、高亮、段落字号和双列图片。
10
12
  - 使用 Vue 2 `CreateElement` 和 `VNodeData`,不是 Vue 3 兼容层。
11
13
  - 支持严格模式和非严格容错渲染。
12
14
  - 拦截危险链接和图片 URL。
@@ -401,19 +403,301 @@ function handleArticleButtonClick(payload: ArticleButtonClickPayload): void {
401
403
  }
402
404
  ```
403
405
 
404
- ## Props
406
+ ## revealedKeys:解锁隐藏内容
407
+
408
+ `revealedKeys` 是由使用方维护的 `string[]`,默认值为 `[]`,通过 `:revealed-keys="revealedKeys"` 传给组件。数组中的字符串与问题节点的 `attrs.revealKey` 精确匹配,表示页面允许显示该问题之后的内容。
409
+
410
+ 组件接收的 `document` 必须是**完整文章 JSON**。隐藏部分已经在这份 JSON 中,只是暂时不创建 DOM;更新 `revealedKeys` 后,组件从原始完整文档重新计算并渲染后续内容,**不会请求新的 JSON,也不会根据 `resourceId` 请求问题数据**。新显示的图片可能触发浏览器正常的图片资源请求。
411
+
412
+ ### 完整 Vue 2 示例:选择后解锁
413
+
414
+ 下面的示例采用“选择即允许”的页面规则:初始显示引言和问题,点击“继续阅读”后,页面将事件中的 `revealKey` 加入列表,组件显示后文并定位到 `details` 段落。也可以点击页面的“直接解锁”按钮,演示从外部控制显示。
415
+
416
+ ```vue
417
+ <script lang="ts">
418
+ import Vue from 'vue'
419
+ import {
420
+ ArticleContentRenderer,
421
+ type ArticleDocument,
422
+ type ArticleRendererRuntime,
423
+ type ResourceQuestionSelectEvent,
424
+ } from 'article-content-renderer-vue2'
425
+ import 'article-content-renderer-vue2/style.css'
426
+
427
+ export default Vue.extend({
428
+ components: { ArticleContentRenderer },
429
+ data() {
430
+ const article: ArticleDocument = {
431
+ type: 'doc',
432
+ content: [
433
+ { type: 'paragraph', content: [{ type: 'text', text: '这段引言始终显示。' }] },
434
+ {
435
+ type: 'resourceQuestion',
436
+ attrs: {
437
+ id: 'question-1',
438
+ resourceId: 'resource-123',
439
+ title: '准备好继续阅读了吗?',
440
+ description: '选择后显示后续内容。',
441
+ options: [{ id: 'read', label: '继续阅读', targetAnchorId: 'details' }],
442
+ hideFollowing: true,
443
+ revealKey: 'article-content',
444
+ },
445
+ },
446
+ {
447
+ type: 'paragraph',
448
+ attrs: { anchorId: 'details' },
449
+ content: [{ type: 'text', text: '这是解锁后显示的正文,始终保存在完整 JSON 中。' }],
450
+ },
451
+ ],
452
+ }
453
+ return {
454
+ article,
455
+ articleKey: 1,
456
+ revealedKeys: [] as string[],
457
+ runtime: null as ArticleRendererRuntime | null,
458
+ }
459
+ },
460
+ methods: {
461
+ rememberRuntime(runtime: ArticleRendererRuntime): void {
462
+ this.runtime = runtime
463
+ },
464
+ handleOptionSelect(event: ResourceQuestionSelectEvent): void {
465
+ // 本示例的业务规则是“选择即允许”。组件只发事件,不会自行解锁。
466
+ this.unlockContent(event.revealKey)
467
+ },
468
+ unlockContent(revealKey: string): void {
469
+ this.revealedKeys = [...new Set([...this.revealedKeys, revealKey])]
470
+ },
471
+ hideContent(revealKey: string): void {
472
+ this.runtime?.cancelPendingNavigation()
473
+ this.revealedKeys = this.revealedKeys.filter((key) => key !== revealKey)
474
+ },
475
+ switchArticle(nextArticle: ArticleDocument): void {
476
+ this.runtime?.cancelPendingNavigation()
477
+ this.revealedKeys = []
478
+ this.articleKey += 1
479
+ this.article = nextArticle
480
+ },
481
+ },
482
+ })
483
+ </script>
484
+
485
+ <template>
486
+ <main>
487
+ <button type="button" @click="unlockContent('article-content')">直接解锁</button>
488
+ <button type="button" @click="hideContent('article-content')">重新隐藏</button>
489
+ <article>
490
+ <ArticleContentRenderer
491
+ :key="articleKey"
492
+ :document="article"
493
+ :article-key="articleKey"
494
+ :revealed-keys="revealedKeys"
495
+ @renderer-ready="rememberRuntime"
496
+ @option-select="handleOptionSelect"
497
+ />
498
+ </article>
499
+ </main>
500
+ </template>
501
+ ```
502
+
503
+ 示例中的 `article-content` 是保存在 JSON 中的固定标识,可以替换为编辑器保存的随机值。选项事件始终返回当前问题的真实 `event.revealKey`,使用方不必预先知道它,也不要假定它等于问题 ID、资源 ID 或目标段落 ID。
504
+
505
+ `option-select` 携带 `{ questionId, resourceId, optionId, option, revealKey, targetAnchorId? }`,其中 `option` 是被点击选项的完整属性。组件先记录目标,再发出事件;默认在宿主更新列表、DOM 和布局就绪后定位。可通过下面的 `onAnchorNavigate` 回调控制滚动时机。没有目标的选项仍可解锁,只是不跳转。外部按钮直接解锁时,若没有待定位任务,则只显示内容。
506
+
507
+ ### 多个问题与重新隐藏
508
+
509
+ 组件按顶层顺序扫描,保留首个未解锁问题本身,然后停止渲染其后的全部内容。假设文章为“引言 → 问题 A → 段落 A → 问题 B → 段落 B”,两个问题都开启 `hideFollowing`,标识分别为 `step-1` 和 `step-2`:
510
+
511
+ | `revealedKeys` | 可见内容 |
512
+ | --- | --- |
513
+ | `[]` | 引言、问题 A |
514
+ | `["step-1"]` | 引言、问题 A、段落 A、问题 B |
515
+ | `["step-2"]` | 引言、问题 A;不能越过前面的未解锁问题 |
516
+ | `["step-1", "step-2"]` | 全文 |
517
+ | `["unknown"]` | 引言、问题 A;未知标识不生效 |
518
+
519
+ 从列表中移除对应标识即可重新隐藏;将列表设为 `[]` 可重新锁定全部隐藏边界。每次都从原始完整文档计算,无需删除或恢复 JSON 节点。`hideFollowing: false` 的问题不会截断正文;没有隐藏问题的旧文章完整显示。同篇文章内所有问题的 `revealKey` 必须唯一,一次允许多个问题时传入多个不同标识。
520
+
521
+ ### 异步业务与文章切换
522
+
523
+ 如果需要业务接口确认,将 `unlockContent(event.revealKey)` 放在业务允许的分支中;请求期间保持列表不变。接口请求由使用方自行发起,渲染器不负责获取业务结果或新的文章 JSON。业务拒绝、失败或主动取消时,调用 `runtime.cancelPendingNavigation()`,避免后续操作触发旧定位;不要在每次选项点击开始时调用它,否则会取消刚记录的本次目标。
524
+
525
+ 切换文章时,使用类似示例中的 `switchArticle(nextArticle)` 方法,清空 `revealedKeys` 和待定位任务,再更新文章与 `articleKey`。`articleKey` 变化会取消旧定位,但**不会替使用方清空传入的解锁列表**。异步请求还需检查文章版本或加载代次、交互序号,忽略旧文章或旧选择的返回结果;重新隐藏和卸载时也要使旧请求失效。完整异步示例见 [Vue 2 资源问题接入](./docs/resource-question-vue2.md#宿主异步业务)。
526
+
527
+ `revealedKeys` 是页面运行时状态,不写入文章 JSON。不同文章可以使用相同固定 `revealKey`,因此不能共享一份全局解锁列表;需要恢复阅读进度时,应由使用方按用户、文章及版本隔离保存。
528
+
529
+ 隐藏只控制展示,完整文章数据仍在客户端;敏感内容应由服务器在鉴权后返回,`revealKey` 不能作为密码或授权凭证。
530
+
531
+ 可运行示例:启动 Demo,在“隐藏内容解锁示例”面板点击“载入解锁示例”,观察选项事件、解锁列表与正文变化。相关源码见 [主 Demo](./demo/App.vue) 和 [异步资源问题 Demo](./demo/ResourceQuestionDemo.vue)。
532
+
533
+ ## onAnchorNavigate:由使用方决定滚动时机
534
+
535
+ `onAnchorNavigate` 是可选的函数 Prop,通过 `:on-anchor-navigate="handleAnchorNavigate"` 传入。它与 `@option-select` 分工不同:`option-select` 用于处理选择和业务解锁,`onAnchorNavigate` 用于决定何时滚动。
536
+
537
+ - **不传回调**:可见目标在点击后直接按默认流程滚动;如果目标尚未渲染,仍需等待 `revealedKeys` 解锁和 DOM、布局就绪。“默认立即滚动”不会自动解锁内容。
538
+ - **传入回调**:在有有效目标的选项被点击后,组件先发出 `option-select`,再调用一次回调。目标可以仍处于隐藏状态;组件不会自动滚动,只有使用方调用 `request.scrollToAnchor()` 后才执行定位。
539
+ - 提前调用 `scrollToAnchor()` 时,组件会等待目标解锁并完成渲染;在目标已显示后调用也可以。调用此方法不会修改 `revealedKeys`,也不会请求新的 JSON。
540
+
541
+ ### 示例:点击页面按钮后再滚动
542
+
543
+ 在上面的 Vue 2 解锁示例中,额外导入 `ResourceQuestionNavigationRequest`,并在 `data` 和 `methods` 中增加以下内容,原有的解锁逻辑继续保留:
544
+
545
+ ```ts
546
+ import type { ResourceQuestionNavigationRequest } from 'article-content-renderer-vue2'
547
+
548
+ // data() 返回的对象中增加:
549
+ pendingNavigation: null as ResourceQuestionNavigationRequest | null
550
+
551
+ // methods 中增加:
552
+ handleAnchorNavigate(request: ResourceQuestionNavigationRequest): void {
553
+ this.pendingNavigation = request
554
+ },
555
+ scrollWhenReady(): void {
556
+ this.pendingNavigation?.scrollToAnchor()
557
+ this.pendingNavigation = null
558
+ },
559
+ cancelNavigation(): void {
560
+ this.pendingNavigation?.cancel()
561
+ this.pendingNavigation = null
562
+ }
563
+ ```
564
+
565
+ 模板中把回调传给组件,再添加操作按钮:
566
+
567
+ ```vue
568
+ <main>
569
+ <button type="button" :disabled="!pendingNavigation" @click="scrollWhenReady">
570
+ 滚动到所选锚点
571
+ </button>
572
+ <article>
573
+ <ArticleContentRenderer
574
+ :key="articleKey"
575
+ :document="article"
576
+ :article-key="articleKey"
577
+ :revealed-keys="revealedKeys"
578
+ :on-anchor-navigate="handleAnchorNavigate"
579
+ @renderer-ready="rememberRuntime"
580
+ @option-select="handleOptionSelect"
581
+ />
582
+ </article>
583
+ </main>
584
+ ```
585
+
586
+ 此时,点击正文选项可以先解锁并显示后文,但不会滚动;点击“滚动到所选锚点”才请求定位。在原有的 `handleOptionSelect`、`hideContent`、`switchArticle` 中也应将 `pendingNavigation` 设为 `null`,清理页面保存的旧句柄;组件自身仍会校验请求是否有效。
587
+
588
+ 回调也可以等待弹窗关闭、动画结束等异步操作,再显式调用 `request.scrollToAnchor()`。**回调返回、Promise 完成或目标变为可见,都不会替代这次显式调用。**
589
+
590
+ ### 回调类型与取消规则
591
+
592
+ ```ts
593
+ interface ResourceQuestionNavigationRequest extends ResourceQuestionSelectEvent {
594
+ targetAnchorId: string
595
+ scrollToAnchor(): void
596
+ cancel(): void
597
+ }
598
+
599
+ type OnAnchorNavigate = (
600
+ request: Readonly<ResourceQuestionNavigationRequest>,
601
+ ) => void | Promise<void>
602
+ ```
603
+
604
+ 请求包含 `questionId`、`resourceId`、`optionId`、`option`、`revealKey` 和 `targetAnchorId`,可通过 `request.option` 读取选项属性。`scrollToAnchor()` 继续使用组件的实例内锚点、`scrollContainer`、`scrollOffset` 和焦点处理。`request.cancel()` 只取消该次点击的定位,不改变解锁列表;也可使用 `renderer-ready` 提供的 `runtime.cancelPendingNavigation()` 取消当前定位。
605
+
606
+ 没有绑定锚点或锚点已不存在时,仍发出 `option-select`,但不会调用 `onAnchorNavigate`。同步在 `option-select` 中取消定位时,也不会再调用该次导航回调。每次有效点击只回调一次,重新渲染不会重复回调。
607
+
608
+ 新选择、文章对象或 `articleKey` 变化、移除解锁标识、主动取消、卸载以及成功滚动后,旧请求的 `scrollToAnchor()` 和 `cancel()` 都不再生效,因此过期的异步回调不会影响新选择。更改回调 Prop 只影响后续点击,已经交给使用方的请求仍需显式执行或取消。
609
+
610
+ 回调抛出异常或返回的 Promise 拒绝时,组件会取消仍有效的本次定位,并通过 `render-error` 报告 `NAVIGATION_CALLBACK_FAILED`,不会退回自动滚动。过期回调的失败不影响新请求。
611
+
612
+ 两个 Demo 的解锁面板均提供“由页面决定滚动时机”开关:关闭时不传回调,开启后通过“滚动到所选锚点”按钮手动调用回调提供的方法。
613
+
614
+ ## Props
615
+
616
+ 资源问题完整接入说明及异步业务示例见 [Vue 2 资源问题接入](./docs/resource-question-vue2.md)。
405
617
 
406
618
  | Prop | 类型 | 默认值 | 说明 |
407
619
  | --- | --- | --- | --- |
408
620
  | `document` | `unknown` | 必填 | Article Content Protocol 文档 |
409
621
  | `protocolVersion` | `number` | `1` | 协议适配器版本 |
410
- | `strict` | `boolean` | `false` | 校验失败时是否停止整篇正文渲染 |
622
+ | `strict` | `boolean` | `false` | 校验失败时是否停止整篇正文渲染 |
623
+ | `revealedKeys` | `string[]` | `[]` | 宿主允许的解锁标识;选项点击不自动修改 |
624
+ | `articleKey` | `string \| number` | `undefined` | 文章版本或加载代次,变化时取消旧定位;宿主仍须重置解锁状态 |
625
+ | `scrollContainer` | `HTMLElement \| (() => HTMLElement \| null)` | `undefined` | 目标滚动容器,省略时滚动页面 |
626
+ | `scrollOffset` | `number` | `0` | 定位时顶部遮挡偏移,单位 px |
627
+ | `onAnchorNavigate` | `OnAnchorNavigate` | `undefined` | 可选滚动回调;传入后须显式调用 `request.scrollToAnchor()`,省略时默认定位 |
411
628
  | `customSlots` | `CustomSlot[]` | `[]` | 配置一个或多个具名插槽的顶层正文插入位置 |
412
629
  | `imageBaseUrl` | `string` | `"https://www.doitme.link/"` | 替换文档图片的默认地址前缀 |
413
630
  | `resolveArticleButtonLink` | `ResolveArticleButtonLink` | `undefined` | 为 text/button 生成完整链接;link 类型不调用 |
414
631
  | `resolveCustomLink` | `ResolveCustomLink` | `undefined` | 仅为 `type: "custom"` 的 link mark 生成完整安全链接 |
415
632
 
416
- ## Events
633
+ ## Events
634
+
635
+ ### option-select:点击选项并获取属性
636
+
637
+ 通过 `@option-select="handleOptionSelect"` 注册点击回调,回调参数的 `event.option` 返回当前选项在文章 JSON 中保存的完整属性:
638
+
639
+ ```ts
640
+ interface ResourceQuestionSelectEvent {
641
+ questionId: string
642
+ resourceId: string
643
+ optionId: string
644
+ option: Readonly<{
645
+ id: string
646
+ label: string
647
+ targetAnchorId?: string
648
+ }>
649
+ revealKey: string
650
+ targetAnchorId?: string
651
+ }
652
+ ```
653
+
654
+ 下面的组件接收文章并展示最近点击选项的属性,可在回调内接入自己的业务操作:
655
+
656
+ ```vue
657
+ <script lang="ts">
658
+ import Vue, { type PropType } from 'vue'
659
+ import ArticleContentRenderer, {
660
+ type ArticleDocument,
661
+ type ResourceQuestionOption,
662
+ type ResourceQuestionSelectEvent,
663
+ } from 'article-content-renderer-vue2'
664
+ import 'article-content-renderer-vue2/style.css'
665
+
666
+ export default Vue.extend({
667
+ components: { ArticleContentRenderer },
668
+ props: {
669
+ article: { type: Object as PropType<ArticleDocument>, required: true },
670
+ },
671
+ data() {
672
+ return { selectedOption: null as Readonly<ResourceQuestionOption> | null }
673
+ },
674
+ methods: {
675
+ handleOptionSelect(event: ResourceQuestionSelectEvent): void {
676
+ this.selectedOption = event.option
677
+ // event.option.id:选项 ID
678
+ // event.option.label:选项文本
679
+ // event.option.targetAnchorId:目标锚点(未绑定时不存在)
680
+ // event.questionId / event.resourceId:所属问题及资源 ID
681
+ },
682
+ },
683
+ })
684
+ </script>
685
+
686
+ <template>
687
+ <main>
688
+ <ArticleContentRenderer :document="article" @option-select="handleOptionSelect" />
689
+ <pre v-if="selectedOption">{{ JSON.stringify(selectedOption, null, 2) }}</pre>
690
+ </main>
691
+ </template>
692
+ ```
693
+
694
+ 每次点击触发一次回调,包括重复点击同一选项、没有绑定锚点或锚点已删除的情况;无需在各个 JSON 选项中配置函数。选项通过所属问题和稳定的 `id` 匹配,同名选项也能区分。事件及其 `option` 是冻结的只读快照副本,不会随之后的文章修改而变化;需要编辑时先复制,例如 `{ ...event.option }`。原有的顶层 `optionId`、`targetAnchorId` 等字段继续保留。
695
+
696
+ 此示例只展示回调收到的属性。宿主业务允许后可更新 `revealedKeys` 来解锁隐藏内容;默认等待 DOM 和布局后定位可见目标。配置 `onAnchorNavigate` 后,还需使用方显式调用 `request.scrollToAnchor()`。两个 Demo 的解锁面板均会展示 `event.option`。
697
+
698
+ ### renderer-ready
699
+
700
+ `renderer-ready` 返回 `ArticleRendererRuntime`,提供 `cancelPendingNavigation()`。Vue 2 函数式组件没有可通过 `ref` 获取的实例,请保存这个运行时句柄,在拒绝、失败或主动取消时调用。
417
701
 
418
702
  ### article-button-click
419
703
 
@@ -435,13 +719,16 @@ interface RenderIssue {
435
719
  code: RenderIssueCode
436
720
  path: string
437
721
  message: string
438
- nodeType?: string
722
+ nodeType?: string
723
+ severity?: 'error' | 'warning'
439
724
  }
440
725
  ```
441
726
 
442
727
  ## 校验和 URL 安全
443
728
 
444
- 校验同时覆盖 JSON 结构、`contentModel` 和跨节点约束:
729
+ 校验同时覆盖 JSON 结构、`contentModel` 和跨节点约束:
730
+
731
+ 使用仓库中的最新 `documentSchema` 校验结构,并保留旧版允许省略的默认字段。重复问题 ID、revealKey、anchorId 或问题内选项 ID 会报错并停止渲染;包含资源问题的文章即使非严格模式也必须通过结构校验。失效目标是 `MISSING_ANCHOR` 警告,不会使整篇文章失效。
445
732
 
446
733
  - `block+`、`listItem+`、`tableRow+` 等内容不能为空。
447
734
  - `codeBlock` 最多包含一个 text 节点。
@@ -488,7 +775,13 @@ npm install
488
775
  npm run dev
489
776
  ```
490
777
 
491
- 默认访问 `http://localhost:5173`。Demo 包含全部主要节点、可编辑 articleButton resolver、严格模式、错误列表和 JSON 查看器。
778
+ 默认访问 `http://localhost:5173`。Demo 包含全部主要节点、可编辑 articleButton resolver、严格模式、错误列表和 JSON 查看器。
779
+
780
+ 资源问题独立 Demo:`http://localhost:5173/?demo=resource-question`,提供两步解锁、异步允许、拒绝、取消及切换文章演示。
781
+
782
+ 两个 Demo 都提供“传入文章 JSON”输入区:粘贴后点击“应用 JSON”更新预览,也可“恢复示例”。解析或协议校验失败会显示错误并保留当前文章;资源问题 Demo 应用成功后会清空解锁状态并取消旧的异步操作和定位。
783
+
784
+ 解锁操作示例:在“隐藏内容解锁示例”面板点击“载入解锁示例”,再点击正文中的“先了解基础”。主 Demo 会把事件的 `revealKey` 加入 `revealedKeys` 并显示后文;面板会同步展示收到的标识和当前列表。每个隐藏边界还提供“解锁此处后文”和“重新隐藏此处后文”按钮,可验证多个边界按顺序生效。应用其他文章会重置解锁状态;独立资源问题 Demo 还可切换延迟允许或拒绝。
492
785
 
493
786
  动态 resolver 编辑器只用于本地 Demo;生产项目应在 Vue/TypeScript 源码中定义 resolver。
494
787
 
@@ -1 +1 @@
1
- :root{--acp-color-text: inherit;--acp-color-muted: #667085;--acp-color-border: #d0d5dd;--acp-color-surface: #f8fafc;--acp-color-accent: #2563eb;--acp-color-accent-hover: #1d4ed8;--acp-color-on-accent: #ffffff;--acp-radius: .375rem;--acp-spacing-block: 1rem;--acp-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace}.acp-paragraph,.acp-heading,.acp-blockquote,.acp-list,.acp-code-block,.acp-horizontal-rule,.acp-image,.acp-article-button,.acp-table-wrapper{box-sizing:border-box}.acp-paragraph,.acp-heading,.acp-blockquote,.acp-list,.acp-code-block,.acp-horizontal-rule,.acp-image,.acp-table-wrapper{margin-block:0 var(--acp-spacing-block)}.acp-paragraph:first-child,.acp-heading:first-child,.acp-blockquote:first-child,.acp-list:first-child,.acp-code-block:first-child,.acp-horizontal-rule:first-child,.acp-image:first-child,.acp-table-wrapper:first-child{margin-block-start:0}.acp-paragraph{color:var(--acp-color-text)}.acp-blockquote{margin-inline:0;padding-inline-start:1rem;border-inline-start:.25rem solid var(--acp-color-border);color:var(--acp-color-muted)}.acp-list{padding-inline-start:1.5rem}.acp-list-item>:last-child{margin-block-end:0}.acp-code-block{overflow-x:auto;padding:1rem;border-radius:var(--acp-radius);background:var(--acp-color-surface);font-family:var(--acp-font-mono);white-space:pre}.acp-mark--code{padding:.1em .25em;border-radius:calc(var(--acp-radius) / 2);background:var(--acp-color-surface);font-family:var(--acp-font-mono)}.acp-horizontal-rule{border:0;border-block-start:1px solid var(--acp-color-border)}.acp-image{display:flex;width:100%}.acp-image--left{justify-content:flex-start}.acp-image--center{justify-content:center}.acp-image--right{justify-content:flex-end}.acp-image__element{display:block;max-width:100%;height:auto}.acp-link,.acp-article-button--text,.acp-article-button--link{color:var(--acp-color-accent);text-decoration:underline;text-underline-offset:.15em}.acp-link:hover,.acp-article-button--text:hover,.acp-article-button--link:hover{color:var(--acp-color-accent-hover)}.acp-link--disabled{cursor:not-allowed;opacity:.55}.acp-article-button{display:inline-flex;align-items:center;justify-content:center;margin-block:0 var(--acp-spacing-block);cursor:pointer}.acp-article-button--button{min-height:2.5rem;padding:.5rem 1rem;border-radius:var(--acp-radius);background:var(--acp-color-accent);color:var(--acp-color-on-accent);text-decoration:none}.acp-article-button--button:hover{background:var(--acp-color-accent-hover)}.acp-article-button--disabled{cursor:not-allowed;opacity:.55}.acp-table-wrapper{width:100%;overflow-x:auto}.acp-table{width:100%;border-collapse:collapse}.acp-table-cell{padding:.625rem .75rem;border:1px solid var(--acp-color-border);vertical-align:top}.acp-table-cell>:last-child{margin-block-end:0}.acp-render-error{padding:.75rem 1rem;border:1px solid #fda29b;border-radius:var(--acp-radius);background:#fffbfa;color:#b42318}
1
+ :root{--acp-color-text: inherit;--acp-color-muted: #667085;--acp-color-border: #d0d5dd;--acp-color-surface: #f8fafc;--acp-color-accent: #2563eb;--acp-color-accent-hover: #1d4ed8;--acp-color-on-accent: #ffffff;--acp-radius: .375rem;--acp-spacing-block: 1rem;--acp-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace}.acp-paragraph,.acp-heading,.acp-blockquote,.acp-list,.acp-code-block,.acp-horizontal-rule,.acp-image,.acp-article-button,.acp-table-wrapper{box-sizing:border-box}.acp-paragraph,.acp-heading,.acp-blockquote,.acp-list,.acp-code-block,.acp-horizontal-rule,.acp-image,.acp-table-wrapper{margin-block:0 var(--acp-spacing-block)}.acp-paragraph:first-child,.acp-heading:first-child,.acp-blockquote:first-child,.acp-list:first-child,.acp-code-block:first-child,.acp-horizontal-rule:first-child,.acp-image:first-child,.acp-table-wrapper:first-child{margin-block-start:0}.acp-paragraph{color:var(--acp-color-text)}.acp-blockquote{margin-inline:0;padding-inline-start:1rem;border-inline-start:.25rem solid var(--acp-color-border);color:var(--acp-color-muted)}.acp-list{padding-inline-start:1.5rem}.acp-list-item>:last-child{margin-block-end:0}.acp-code-block{overflow-x:auto;padding:1rem;border-radius:var(--acp-radius);background:var(--acp-color-surface);font-family:var(--acp-font-mono);white-space:pre}.acp-mark--code{padding:.1em .25em;border-radius:calc(var(--acp-radius) / 2);background:var(--acp-color-surface);font-family:var(--acp-font-mono)}.acp-horizontal-rule{border:0;border-block-start:1px solid var(--acp-color-border)}.acp-image{display:flex;width:100%}.acp-image--left{justify-content:flex-start}.acp-image--center{justify-content:center}.acp-image--right{justify-content:flex-end}.acp-image__element{display:block;max-width:100%;height:auto}.acp-image-row{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--acp-image-gap, 1rem);align-items:start}.acp-image-row>.acp-image{min-width:0}.acp-resource-question{min-inline-size:0;margin:0 0 var(--acp-spacing-block);padding:1rem;border:1px solid var(--acp-color-border);border-radius:var(--acp-radius);color:var(--acp-color-text)}.acp-resource-question__title{padding-inline:.25rem;font-size:1.125em;font-weight:600;white-space:pre-wrap;overflow-wrap:anywhere}.acp-resource-question__description,.acp-resource-question__placeholder{margin:0 0 .75rem;color:var(--acp-color-muted);white-space:pre-wrap;overflow-wrap:anywhere}.acp-resource-question__options{display:grid;gap:.5rem}.acp-resource-question__option{padding:.75rem 1rem;border:1px solid var(--acp-color-border);border-radius:var(--acp-radius);background:var(--acp-color-surface);color:inherit;font:inherit;text-align:start;white-space:pre-wrap;overflow-wrap:anywhere;cursor:pointer}.acp-resource-question__option:hover{border-color:var(--acp-color-accent)}.acp-resource-question__option:focus-visible,.acp-paragraph[data-anchor-id]:focus,.acp-heading[data-anchor-id]:focus{outline:2px solid var(--acp-color-accent);outline-offset:3px}.acp-link,.acp-article-button--text,.acp-article-button--link{color:var(--acp-color-accent);text-decoration:underline;text-underline-offset:.15em}.acp-link:hover,.acp-article-button--text:hover,.acp-article-button--link:hover{color:var(--acp-color-accent-hover)}.acp-link--disabled{cursor:not-allowed;opacity:.55}.acp-article-button{display:inline-flex;align-items:center;justify-content:center;margin-block:0 var(--acp-spacing-block);cursor:pointer}.acp-article-button--button{min-height:2.5rem;padding:.5rem 1rem;border-radius:var(--acp-radius);background:var(--acp-color-accent);color:var(--acp-color-on-accent);text-decoration:none}.acp-article-button--button:hover{background:var(--acp-color-accent-hover)}.acp-article-button--disabled{cursor:not-allowed;opacity:.55}.acp-table-wrapper{width:100%;overflow-x:auto}.acp-table{width:100%;border-collapse:collapse}.acp-table-cell{padding:.625rem .75rem;border:1px solid var(--acp-color-border);vertical-align:top}.acp-table-cell>:last-child{margin-block-end:0}.acp-render-error{padding:.75rem 1rem;border:1px solid #fda29b;border-radius:var(--acp-radius);background:#fffbfa;color:#b42318}