diff options
| author | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
|---|---|---|
| committer | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
| commit | bce557cc2dc767628bed6aac87301a1be7c5431b (patch) | |
| tree | b51a051228d01fe3306cd7626d4a96768aadb944 /src/components/jet/item/Annotation | |
init commit
Diffstat (limited to 'src/components/jet/item/Annotation')
3 files changed, 277 insertions, 0 deletions
diff --git a/src/components/jet/item/Annotation/AnnotationItem.svelte b/src/components/jet/item/Annotation/AnnotationItem.svelte new file mode 100644 index 0000000..38bb269 --- /dev/null +++ b/src/components/jet/item/Annotation/AnnotationItem.svelte @@ -0,0 +1,17 @@ +<script lang="ts"> + import { type Annotation } from '@jet-app/app-store/api/models'; + import ModernAnnotationItemRenderer from '~/components/jet/item/Annotation/ModernAnnotationItemRenderer.svelte'; + import LegacyAnnotationRenderer from '~/components/jet/item/Annotation/LegacyAnnotationRenderer.svelte'; + + export let item: Annotation; + + $: ({ items, items_V3, linkAction, summary } = item); + + $: shouldRenderModernAnnotation = items_V3.length > 0; +</script> + +{#if shouldRenderModernAnnotation} + <ModernAnnotationItemRenderer items={items_V3} {summary} /> +{:else} + <LegacyAnnotationRenderer {items} {linkAction} /> +{/if} diff --git a/src/components/jet/item/Annotation/LegacyAnnotationRenderer.svelte b/src/components/jet/item/Annotation/LegacyAnnotationRenderer.svelte new file mode 100644 index 0000000..fc6586f --- /dev/null +++ b/src/components/jet/item/Annotation/LegacyAnnotationRenderer.svelte @@ -0,0 +1,146 @@ +<script lang="ts"> + import { isSome } from '@jet/environment'; + import { + type AnnotationItem, + type Action, + isFlowAction, + } from '@jet-app/app-store/api/models'; + import LinkWrapper from '~/components/LinkWrapper.svelte'; + + export let items: AnnotationItem[]; + export let linkAction: Action | undefined; + + const shouldRenderAsDefinitionList = (items: AnnotationItem[]) => + !!items[0]?.heading; + + const shouldRenderAsOrderedList = (items: AnnotationItem[]) => + !!items[0]?.textPairs; + + const shouldRenderAsUnorderedList = (items: AnnotationItem[]) => + !items[0]?.text; + + const shouldRenderAsDefinitionListWithHeading = (items: AnnotationItem[]) => + items[0]?.text && items[1]?.heading; +</script> + +{#if shouldRenderAsDefinitionList(items)} + <dl class="secondary-definition-list"> + {#each items as annotationItem} + <dt>{annotationItem.heading}</dt> + <dd>{annotationItem.text}</dd> + {/each} + </dl> +{:else if shouldRenderAsOrderedList(items)} + <ol> + {#each items as annotationItem} + {#if annotationItem.textPairs} + {#each annotationItem.textPairs as [text, subtext]} + <li> + <span class="text">{text}</span> + <span class="subtext">{subtext}</span> + </li> + {/each} + {:else} + <li>{annotationItem.text}</li> + {/if} + {/each} + </ol> +{:else if shouldRenderAsUnorderedList(items)} + <ul> + {#each items as annotationItem} + <li> + <span class="text"> + {annotationItem.text} + </span> + </li> + {/each} + </ul> +{:else if shouldRenderAsDefinitionListWithHeading(items)} + {@const [heading, ...remainingItems] = items} + <dd> + <p class="secondary-definition-list-heading">{heading.text}</p> + + <dl class="secondary-definition-list"> + {#each remainingItems as annotationItem} + <dt>{annotationItem.heading}</dt> + <dd>{annotationItem.text}</dd> + {/each} + </dl> + </dd> +{:else} + <dd> + <ul> + {#each items as annotationItem} + <li>{annotationItem.text}</li> + {/each} + </ul> + {#if isSome(linkAction) && isFlowAction(linkAction)} + <LinkWrapper action={linkAction}> + {linkAction.title} + </LinkWrapper> + {/if} + </dd> +{/if} + +<style> + dt { + color: var(--systemSecondary); + font: var(--body-tall); + } + + dd { + white-space: pre-line; + font: var(--body-tall); + } + + ol { + counter-reset: section; + } + + ol li { + display: table-row; + font: var(--body-tall); + } + + ol li::before { + counter-increment: section; + content: counter(section) '.'; + display: table-cell; + padding-inline-end: 6px; + } + + ol li .text { + display: table-cell; + width: 100%; + } + + ol li .subtext { + display: table-cell; + } + + .secondary-definition-list-heading { + margin-bottom: 16px; + } + + .secondary-definition-list dt { + color: var(--systemPrimary); + font: var(--body-emphasized); + } + + .secondary-definition-list dd:not(:last-of-type) { + margin-bottom: 16px; + } + + dd li:not(:last-of-type) { + margin-bottom: 16px; + } + + dd :global(a) { + color: var(--keyColor); + text-decoration: none; + } + + dd :global(a:hover) { + text-decoration: underline; + } +</style> diff --git a/src/components/jet/item/Annotation/ModernAnnotationItemRenderer.svelte b/src/components/jet/item/Annotation/ModernAnnotationItemRenderer.svelte new file mode 100644 index 0000000..20611d3 --- /dev/null +++ b/src/components/jet/item/Annotation/ModernAnnotationItemRenderer.svelte @@ -0,0 +1,114 @@ +<script lang="ts"> + import type { AnnotationItem_V3 } from '@jet-app/app-store/api/models'; + import { sanitizeHtml } from '@amp/web-app-components/src/utils/sanitize-html'; + import SystemImage, { + isSystemImageArtwork, + } from '~/components/SystemImage.svelte'; + import LinkWrapper from '~/components/LinkWrapper.svelte'; + + export let items: AnnotationItem_V3[]; + export let summary: string | undefined; + + const formatStyledText = (text: string): string => { + return ( + text + // Replace \n with <br> + .replace(/\n/g, '<br>') + // Replace **text** with <strong>text</strong> + .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') + ); + }; +</script> + +<ul> + {#each items as annotationItem} + <li> + {#if annotationItem.$kind === 'textEncapsulation'} + <div class="text-encapsulation"> + {annotationItem.text} + </div> + {:else if annotationItem.$kind === 'linkableText'} + <div class="styled-text"> + {@html sanitizeHtml( + formatStyledText( + annotationItem.linkableText.styledText.rawText, + ), + )} + </div> + {:else if annotationItem.$kind === 'artwork'} + {#if isSystemImageArtwork(annotationItem.artwork)} + <div class="artwork-wrapper" aria-label={summary}> + <SystemImage artwork={annotationItem.artwork} /> + </div> + {/if} + {:else if annotationItem.$kind === 'textPair'} + <div class="text-pair"> + <span>{annotationItem.leadingText}</span> + <span> + {annotationItem.trailingText} + </span> + </div> + {:else if annotationItem.$kind === 'button'} + <div class="button-wrapper"> + <LinkWrapper action={annotationItem.action}> + {annotationItem.action.title} + </LinkWrapper> + </div> + {:else if annotationItem.$kind === 'spacer'} + <div class="spacer" /> + {/if} + </li> + {/each} +</ul> + +<style> + li { + font: var(--body-tall); + } + + .styled-text :global(strong) { + color: var(--systemPrimary); + font: var(--body-emphasized); + } + + .text-encapsulation { + width: fit-content; + color: var(--keyColor); + border: 1px solid; + border-radius: 3px; + padding-inline: 3px; + border-color: var(--keyColor); + margin-block: 3px; + } + + .artwork-wrapper :global(svg) { + height: 18px; + width: 18px; + margin-top: 4px; + } + + .spacer { + height: 16px; + } + + .button-wrapper :global(a) { + color: var(--keyColor); + text-decoration: none; + } + + .button-wrapper :global(a:hover) { + text-decoration: underline; + } + + .button-wrapper :global(a) :global(.external-link-arrow) { + width: 7px; + height: 7px; + fill: var(--keyColor); + margin-top: 3px; + } + + .text-pair { + display: flex; + justify-content: space-between; + } +</style> |
