@sequencestudio/web-core 0.0.0-stage → 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.
Files changed (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +183 -2
  3. package/dist/access.d.ts +29 -0
  4. package/dist/access.js +46 -0
  5. package/dist/badges.d.ts +78 -0
  6. package/dist/badges.js +89 -0
  7. package/dist/card-model.d.ts +70 -0
  8. package/dist/card-model.js +1 -0
  9. package/dist/cards.d.ts +141 -0
  10. package/dist/cards.js +383 -0
  11. package/dist/class-names.d.ts +63 -0
  12. package/dist/class-names.js +62 -0
  13. package/dist/clicks.d.ts +33 -0
  14. package/dist/clicks.js +12 -0
  15. package/dist/component-drag.d.ts +27 -0
  16. package/dist/component-drag.js +225 -0
  17. package/dist/component-items-route.d.ts +65 -0
  18. package/dist/component-items-route.js +80 -0
  19. package/dist/component-loading.d.ts +273 -0
  20. package/dist/component-loading.js +367 -0
  21. package/dist/detail-header.d.ts +102 -0
  22. package/dist/detail-header.js +178 -0
  23. package/dist/events.d.ts +50 -0
  24. package/dist/events.js +76 -0
  25. package/dist/game.d.ts +49 -0
  26. package/dist/game.js +62 -0
  27. package/dist/hero.d.ts +45 -0
  28. package/dist/hero.js +89 -0
  29. package/dist/images.d.ts +42 -0
  30. package/dist/images.js +81 -0
  31. package/dist/index.d.ts +25 -0
  32. package/dist/index.js +25 -0
  33. package/dist/layout.d.ts +166 -0
  34. package/dist/layout.js +164 -0
  35. package/dist/loading.d.ts +23 -0
  36. package/dist/loading.js +25 -0
  37. package/dist/placed-items-route.d.ts +96 -0
  38. package/dist/placed-items-route.js +120 -0
  39. package/dist/placed-loading.d.ts +153 -0
  40. package/dist/placed-loading.js +214 -0
  41. package/dist/placed.d.ts +55 -0
  42. package/dist/placed.js +76 -0
  43. package/dist/player/action-feedback.d.ts +54 -0
  44. package/dist/player/action-feedback.js +161 -0
  45. package/dist/player/annotations.d.ts +46 -0
  46. package/dist/player/annotations.js +101 -0
  47. package/dist/player/chrome.d.ts +95 -0
  48. package/dist/player/chrome.js +222 -0
  49. package/dist/player/controls.d.ts +33 -0
  50. package/dist/player/controls.js +24 -0
  51. package/dist/player/delivery-type.d.ts +11 -0
  52. package/dist/player/delivery-type.js +14 -0
  53. package/dist/player/format-playback-time.d.ts +1 -0
  54. package/dist/player/format-playback-time.js +12 -0
  55. package/dist/player/fullscreen.d.ts +23 -0
  56. package/dist/player/fullscreen.js +136 -0
  57. package/dist/player/index.d.ts +36 -0
  58. package/dist/player/index.js +36 -0
  59. package/dist/player/live-recovery.d.ts +58 -0
  60. package/dist/player/live-recovery.js +96 -0
  61. package/dist/player/live.d.ts +22 -0
  62. package/dist/player/live.js +24 -0
  63. package/dist/player/markers.d.ts +42 -0
  64. package/dist/player/markers.js +74 -0
  65. package/dist/player/playback-analytics.d.ts +63 -0
  66. package/dist/player/playback-analytics.js +354 -0
  67. package/dist/player/playback-authorization.d.ts +23 -0
  68. package/dist/player/playback-authorization.js +80 -0
  69. package/dist/player/playback-quality.d.ts +28 -0
  70. package/dist/player/playback-quality.js +51 -0
  71. package/dist/player/player-keyboard.d.ts +29 -0
  72. package/dist/player/player-keyboard.js +220 -0
  73. package/dist/player/player-settings.d.ts +22 -0
  74. package/dist/player/player-settings.js +1 -0
  75. package/dist/player/progress-reporter-state.d.ts +108 -0
  76. package/dist/player/progress-reporter-state.js +71 -0
  77. package/dist/player/progress-reporter.d.ts +20 -0
  78. package/dist/player/progress-reporter.js +428 -0
  79. package/dist/player/progress-retry.d.ts +8 -0
  80. package/dist/player/progress-retry.js +23 -0
  81. package/dist/player/random-id.d.ts +14 -0
  82. package/dist/player/random-id.js +24 -0
  83. package/dist/player/ref.d.ts +8 -0
  84. package/dist/player/ref.js +1 -0
  85. package/dist/player/scrub-view.d.ts +51 -0
  86. package/dist/player/scrub-view.js +59 -0
  87. package/dist/player/seek.d.ts +5 -0
  88. package/dist/player/seek.js +17 -0
  89. package/dist/player/shaka-error.d.ts +70 -0
  90. package/dist/player/shaka-error.js +257 -0
  91. package/dist/player/shaka-playback.d.ts +73 -0
  92. package/dist/player/shaka-playback.js +707 -0
  93. package/dist/player/shaka-player-support.d.ts +125 -0
  94. package/dist/player/shaka-player-support.js +142 -0
  95. package/dist/player/shaka-text-tracks.d.ts +10 -0
  96. package/dist/player/shaka-text-tracks.js +35 -0
  97. package/dist/player/stall-tracker.d.ts +41 -0
  98. package/dist/player/stall-tracker.js +58 -0
  99. package/dist/player/styles.css +1712 -0
  100. package/dist/player/telemetry.d.ts +245 -0
  101. package/dist/player/telemetry.js +11 -0
  102. package/dist/player/test-ids.d.ts +93 -0
  103. package/dist/player/test-ids.js +65 -0
  104. package/dist/player/track-language.d.ts +22 -0
  105. package/dist/player/track-language.js +25 -0
  106. package/dist/player/track-options.d.ts +25 -0
  107. package/dist/player/track-options.js +106 -0
  108. package/dist/player/track-preference.d.ts +24 -0
  109. package/dist/player/track-preference.js +59 -0
  110. package/dist/player/track-settings.d.ts +61 -0
  111. package/dist/player/track-settings.js +305 -0
  112. package/dist/player/video-store.d.ts +28 -0
  113. package/dist/player/video-store.js +89 -0
  114. package/dist/player/viewer-seek.d.ts +18 -0
  115. package/dist/player/viewer-seek.js +25 -0
  116. package/dist/player/volume-preference.d.ts +20 -0
  117. package/dist/player/volume-preference.js +65 -0
  118. package/dist/presentation.d.ts +112 -0
  119. package/dist/presentation.js +125 -0
  120. package/dist/product.d.ts +31 -0
  121. package/dist/product.js +60 -0
  122. package/dist/recency.d.ts +46 -0
  123. package/dist/recency.js +156 -0
  124. package/dist/renderers.d.ts +64 -0
  125. package/dist/renderers.js +58 -0
  126. package/dist/revive.d.ts +17 -0
  127. package/dist/revive.js +64 -0
  128. package/dist/route-support.d.ts +72 -0
  129. package/dist/route-support.js +129 -0
  130. package/dist/server-browser.d.ts +1 -0
  131. package/dist/server-browser.js +4 -0
  132. package/dist/server.d.ts +2 -0
  133. package/dist/server.js +4 -0
  134. package/dist/shelf-layout.d.ts +88 -0
  135. package/dist/shelf-layout.js +96 -0
  136. package/dist/test-ids.d.ts +159 -0
  137. package/dist/test-ids.js +170 -0
  138. package/dist/theme-config.d.ts +32 -0
  139. package/dist/theme-config.js +63 -0
  140. package/dist/tokens.d.ts +140 -0
  141. package/dist/tokens.js +224 -0
  142. package/package.json +52 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Axonista Limited
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,184 @@
1
- # Temporary Holding Version
1
+ # @sequencestudio/web-core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The framework-independent core for sites that render Sequence layouts. React and Svelte
4
+ renderers build on it; it has no UI framework of its own.
5
+
6
+ It holds:
7
+
8
+ - **The player's core**, in the separate `@sequencestudio/web-core/player` entry point:
9
+ control-slot placement, scrub bar annotations, test ids, telemetry types, the viewer's track and
10
+ volume preferences, and the video utilities (live edge and recovery, stalls, seeking,
11
+ Shaka error normalizing, text tracks, delivery type, playback authorization). It uses
12
+ Shaka's types only; `shaka-player` is an optional peer dependency. The main entry point
13
+ never imports it, apart from the player's test id defaults, which are the `player`
14
+ group of `DEFAULT_TEST_IDS`. `PLAYER_TOKENS` lists the player's tokens.
15
+
16
+ - **The adapter.** `mapLayout`, `mapLayoutComponent` and `appendLayoutComponentItems` turn Client Delivery
17
+ responses, as parsed by `@sequencestudio/sdk`, into components and cards. Cards carry
18
+ a `target` describing where they lead; each site builds its own URLs from it.
19
+ Inline rich-text components keep their `content` payload and use the `rich-text`
20
+ renderer; they have no cards, pagination page or deferred source.
21
+ Each component retains its original typed response as `raw` for renderer overrides.
22
+ Unknown component-delivery variants are skipped without losing supported components.
23
+ `LayoutModel.facets` carries the layout's filter terms and counts when it was requested
24
+ with facets, passed through `mapLayout` untouched; it is absent when they were not
25
+ requested or could not be computed. `mapLayout(response, query)` keeps the query the
26
+ layout was requested with as `LayoutModel.query` (`componentItemsQuery`): its locale, filters,
27
+ aspects and total count, and `itemsPerComponent` as `limit`, which every page of its components
28
+ repeats. `mapCards(items, imagePurpose, aspect)` maps the card lists
29
+ of other responses, such as search results, a container's children, related content or
30
+ recommendations (`StandaloneCardItem`), into the same cards, and leaves out items it does
31
+ not recognize.
32
+ - **Placed renderers.** `componentModel(options)` builds the component a renderer's
33
+ `SequenceShelf` or `SequenceGrid` draws from a site's own items
34
+ (`PlacedComponentOptions`): `kind` (`shelf` or `grid`), `id`, the raw `items`, `card`
35
+ (`stacked`, `inline` for grids, or a site card's name), `size`, `variant`,
36
+ `imagePurpose`, `columns`, `showTitles`, `title`, `subtitle`, `from`, `currentId` and
37
+ `progress`. It maps the items at the chosen image purpose only (default the size's
38
+ poster, `poster-16-9` for inline cards), leaves out unknown items and repeats, applies
39
+ `progress` (a share, or `{ completionRatio, resumeFromSeconds }` whose resume point goes
40
+ on an asset card's link target) and the `now-playing` badge, and returns a `LayoutComponentModel` with `placement: 'page'`
41
+ and `from`. Inline grids get `presentation.cardLayout: 'inline'` and one column at every
42
+ breakpoint unless `columns` sets more. `numberedTitle(card)` gives an inline card's
43
+ title, `3. The Rivalry` when it has an episode number.
44
+ - **The presentation model.** `resolvePresentation` applies Sequence's defaults and the
45
+ card size table (`WEB_CARD_SIZES`): aspect ratio, cards per page or grid columns per
46
+ breakpoint, image purpose and title visibility. `BREAKPOINTS` are `sm` 640, `md` 768,
47
+ `lg` 1024, `xl` 1280 and `2xl` 1536; each size shows one more card at `2xl` than at
48
+ `xl`, the adaptive size aside, and `GAME_CARD_COLUMNS` shows 4. `ColumnsPerBreakpoint`'s
49
+ `2xl` key is optional: without it the `xl` count carries on (`columns2xl`).
50
+ `columnVariables(columns)` gives the `--sq-cols-*` custom properties a renderer sets.
51
+ Card variants retain the Sport Event treatment and fall back to standard for
52
+ unsupported values. Schedule-event cards preserve requested profile/taxonomy context.
53
+ - **Custom renderers.** A component names a site-specific renderer in
54
+ `presentation.custom.renderer`; the rest of `presentation.custom` is passed through.
55
+ `resolveLayoutComponentRenderer` picks a component's renderer, and `resolveCardComponent` a shelf's or
56
+ grid's site card, from the site's choice and then `presentation.custom.card`.
57
+ `SiteCardProps` is what a site card receives (`card`, its `component`, `position`,
58
+ `loading`, `sizes`), and `SiteCardConfig` pairs a card renderer with its `columns`.
59
+ `SiteComponentPlacement` and `placeSiteComponents` place a site's own components among the shown components.
60
+ - **Shelf layout.** `shelfMetrics`, `shelfOffset` and `pageAfterDrag` for card widths,
61
+ spacing and paging. Renderers own the DOM, events and motion.
62
+ - **Design tokens.** `COLOR_ROLES` and generators for CSS custom properties, Tailwind v4
63
+ and Tailwind 3, plus `DESIGN_TOKENS` for type per text role (body, component title, card title,
64
+ button: family, size, line height, weight), shape (card radius and border) and spacing
65
+ (`component-side`, the space at a component's sides, and `card-gap`, between cards); body text has a
66
+ font family there, and `BODY_TYPE_TOKENS` names its size, line height and weight, which
67
+ have no defaults so text keeps the page's own. `CARD_HOVER_GROWTH_MAX` is the most a card
68
+ grows on hover per column in its component, which `card-hover-growth` scales from 0 to 1; and
69
+ `COMPONENT_TOKENS` for single parts such as `card-background` and `live-background`,
70
+ each with its default, a role or a value. `designTokensToCss` writes the tokens a site
71
+ sets. Every custom property starts with `--sq-` (`cssVar('surface-primary')`), so none
72
+ collides with a site's own. `STABLE_CLASS_NAMES` lists the class names both renderers put on
73
+ the main parts, for site CSS where no token fits. The core ships no brand colors. `themesFromConfig`
74
+ reads a site's themes from app config `settings.themes` over the site's own defaults and
75
+ accepts only plain color values. The API does not deliver `settings.themes` yet, so until
76
+ it does, sites get their defaults.
77
+ - **Links and clicks.** `CardHrefContext` is the second argument a site's `cardHref`
78
+ receives: the `component` a card is in and, for a placed renderer, `from`, the container it
79
+ lists (`CardSourceContainer`); `cardHrefContext(component)` builds it. `CardClickEvent` carries
80
+ `component`, `card`, `position`, `source` (`shelf`, `grid`, `adaptive-shelf`,
81
+ `adaptive-header` or `card`) and `placement` (`layout` or `page`, from
82
+ `component.placement`). `componentCardClick`, `cardGridClick` and `cardClick` build them.
83
+ - **Card targets.** A Continue Watching container card carries `target.play`, the asset
84
+ and video assignment the API picked to resume or start. A schedule event card targets
85
+ the occurrence (`type: 'schedule-event'`), which plays through the schedule event
86
+ playback endpoint; `assetId` names the asset behind it.
87
+ - **Access.** Cards for gated assets, Continue Watching target assets, and schedule events carry the Client Delivery access
88
+ policy. `accessState(card.access, viewer)` gives `open`, `sign-in` or `locked` from the
89
+ viewer's sign-in and entitlements. It is fail-closed: an unsupported policy version or
90
+ a malformed policy is locked. Playback authorization stays with the server.
91
+ - **Component context.** `componentItemsContext(component, { gameCardFields, componentContext })` gives the context
92
+ selectors a component requests with its items: the game card selectors (`gameCardContext`) for
93
+ a `sport-event` component when the site sets `gameCardFields`, then the site's `componentContext`
94
+ selectors, without repeats. `needsComponentItems` says whether a component is fetched in the browser:
95
+ deferred components, and components that request context, even when their items came embedded. The
96
+ renderers hand the selectors to the site's `loadComponentItems` as `ComponentItemsRequest.context`.
97
+ - **Component loading.** The core never calls Sequence and holds no keys. A `ComponentItemsRequest`
98
+ carries the `layout` (id, slug and component query), the `component`, the `cursor`, the `context`
99
+ and a `signal`. `componentItemsPath(request)` gives the Client Delivery path for it, by layout
100
+ id, `componentItemsSdkRequest(request)` the request as the SDK's `getLayoutComponentItems` takes it,
101
+ and `componentItemsRequestQuery(request)` only its query, so a next page repeats its first
102
+ page's query. `createComponentItemsLoader({ basePath, headers, fetch })` is a `loadComponentItems`
103
+ for the site's settings: in the browser it fetches each page from the site's own
104
+ component-items route (default `/api/sequence`), and gives a viewer component an empty page without
105
+ a request.
106
+ - **Component-items route.** `@sequencestudio/web-core/server` exports
107
+ `createComponentItemsRoute({ client, maxLimit, cacheControl, onError })`, the site's server
108
+ route for those pages: a function from a standard `Request` to a `Response`, mounted at
109
+ `.../layouts/[layoutRef]/components/[componentId]/items`. It forwards only component parameters within
110
+ `COMPONENT_ITEMS_ROUTE_LIMITS`, never the visitor's headers, passes Sequence's 4xx through
111
+ and answers 502 otherwise. It reads with the SDK's `getLayoutComponentItems(...).withResponse()` and
112
+ caches as Sequence says: a `public` page with Sequence's `Cache-Control`, or the site's
113
+ `cacheControl`, and anything else as `private, no-store`, so a preview or viewer client is
114
+ safe. The entry throws if a browser build imports
115
+ it. The layouts guide in `@sequencestudio/web-react` or `@sequencestudio/web-svelte` (`docs/react/react-2-layouts.md`, `docs/svelte/svelte-2-layouts.md`) mounts it.
116
+ - **Placed loading.** `containerItems(ref, query)` and `catalogSearch(q, query)` build a placed
117
+ renderer's `source`. `createPlacedItemsLoader({ basePath, headers, fetch })` is a
118
+ `loadPlacedItems` for the site's settings: in the browser it fetches each page from the site's
119
+ own container and catalog routes (default `/api/sequence`), and gives a search under 3
120
+ characters an empty page without a request. `createPlacedItems({ source, getLoader, initial })` is
121
+ the framework-free state behind a placed renderer given a source: its first page, retry, next
122
+ pages and a failed next page that ends paging. It reads `getLoader()` for each request, so a
123
+ loader the site sets after mount is used. `@sequencestudio/web-core/server` exports
124
+ `createContainerItemsRoute` and `createCatalogSearchRoute`, mounted at `.../containers/[containerRef]`
125
+ and `.../catalog`, with the component-items route's options and rules: they forward only their own
126
+ parameters within `PLACED_ITEMS_ROUTE_LIMITS`, read with the SDK's `getContainer` and
127
+ `listCatalog` through `.withResponse()`, cache as Sequence says (a route's `cacheControl` gets
128
+ the page and Sequence's own header), and pass Sequence's 4xx through with its error `code`. The placed components guide in either renderer package
129
+ (`docs/react/react-3-placed-components.md`, `docs/svelte/svelte-3-placed-components.md`) mounts them.
130
+ - **Viewer components.** `isViewerComponent` picks Continue Watching, which a site reads with the viewer's own token. `refreshViewerComponents()` loads every viewer
131
+ component on show again, for a site to call after recording progress; renderers listen with
132
+ `onRefreshViewerComponents`. `discoveryView(states, layoutState)` decides what a discovery area
133
+ draws: the empty, unavailable or failed-layout notice, and whether its components show, from
134
+ its components' load states and the site's `LayoutState` (`loading`, `failed`, `retry`).
135
+ `loadWithRetry` retries a failed load
136
+ once, unless it was rejected: `componentLoadError(status)` gives a `ComponentLoadRejectedError` for a
137
+ 4xx other than 408 and 429, and `isRejectedComponentLoad` also reads an SDK error's
138
+ `statusCode`.
139
+ - **Component headings.** `component.title` is the presentation title, else the curated source
140
+ title, trimmed. Untitled components keep no title; their optional subtitle or heading
141
+ link can still render. `component.presentation.showHeading` defaults
142
+ to true. When false, React and Svelte omit the title, subtitle, heading link and
143
+ their wrapper, including during loading and unavailable states. The model keeps
144
+ the copy for accessible region names; custom renderers must honor the flag too.
145
+ Card titles use the separate `component.presentation.showTitles` setting.
146
+ - **Continue Watching containers.** A series or collection card with a play target reads
147
+ as the episode it resumes: the target asset's title, a subtitle of its season and
148
+ episode and the container's title (`S1:E02 · The Dawn`), and the target asset's image
149
+ before the container's. `seasonEpisodeLabel(season, episode)` formats the first part.
150
+ - **Event durations.** A schedule-event card shows its asset's duration beside its
151
+ UPCOMING or LIVE badge, except for a movie.
152
+ - **Products.** `ProductModel` and `productFromCard` give a product overlay a product to
153
+ show (title, image, price, stock, shop link) instead of asset metadata. Prices use each
154
+ currency's minor-unit exponent. Price formatting reuses up to 32 locale/currency
155
+ formatters, evicting the least recently used pair. Only a schedule event's explicit
156
+ status sets a live badge.
157
+ - **Badges.** `BadgeKind` is `live`, `upcoming`, `duration`, `collection`, `label` or
158
+ `now-playing`; `DEFAULT_BADGE_PRESETS` places `now-playing` top left in the `strong`
159
+ style.
160
+ - **Test ids.** `DEFAULT_TEST_IDS` holds the `data-testid` every renderer emits by default.
161
+ `resolveTestIds(siteTestIds)` merges a site's own over them key by key within each
162
+ group, where `null` removes an id, a group or, as `testIds: null`, every id.
163
+ `componentTestId`, `cardTestId` and `badgeTestId` resolve the ids that are functions.
164
+
165
+ Images: pass `card.image` to the SDK's `selectImageUrl` or `imageSrcSet`, with
166
+ `imageSizesFor(component.presentation.columns)` as the `sizes` attribute, which includes the
167
+ `2xl` step. `card.image` is the component's image purpose only (as sent, even one this version
168
+ does not know), or without one the size's default, `poster-2-3` for 2:3 sizes and
169
+ `poster-16-9` for the rest; a card without it has no `card.image`. Continue Watching cards
170
+ try `poster-16-9`, `backdrop-16-9`, then `thumbnail`. `mapLayout` leaves out an adaptive
171
+ component with no purpose. `mapCard(item, purpose, { exactImage: true })` maps the same way.
172
+
173
+ Components filled on the server: `fillComponents(layout, { load, fill, gameCardFields,
174
+ componentContext })` reads the first page of each component that needs items, through a loader with
175
+ `loadComponentItems`'s signature and the request the browser would make (`load: (request) =>
176
+ sequence.getLayoutComponentItems(componentItemsSdkRequest(request))`), and sets it with
177
+ `fillComponent`. A component whose read fails is passed on unfilled and listed in `failures`; viewer
178
+ components are never filled, and `fillComponent` returns one unchanged. `fillComponent(component, page)` sets a component's first page
179
+ of items and marks it `filled`, so it renders in the first HTML and `needsComponentItems` is
180
+ false for it; SequenceLayout still refreshes it (after `refreshDelayMs`,
181
+ `refreshAfterSeconds` plus up to 15%) and pages it. A component's remembered first page is keyed
182
+ by its layout, component query, component, context and, for a viewer component, the viewer.
183
+
184
+ The SDK is a peer dependency used for types; sites also use it to fetch data.
@@ -0,0 +1,29 @@
1
+ import type { ClientDeliveryAssetAccessPolicy } from '@sequencestudio/sdk';
2
+ /** What a card shows for access: playable, sign in first, or locked. */
3
+ export type AccessState = 'open' | 'sign-in' | 'locked';
4
+ export interface ViewerAccess {
5
+ signedIn: boolean;
6
+ /**
7
+ * A stable id for the signed-in viewer, such as the session's user id. With it, viewer
8
+ * components such as Continue Watching are remembered between visits for this viewer only;
9
+ * without it, they are not remembered.
10
+ */
11
+ id?: string;
12
+ /** The viewer's entitlement keys, from /me/entitlements. Undefined when they could not be resolved. */
13
+ entitlements?: readonly string[];
14
+ }
15
+ /** Whether this core can read a policy: the supported version, well formed. */
16
+ export declare function isReadablePolicy(policy: ClientDeliveryAssetAccessPolicy): boolean;
17
+ /**
18
+ * Whether a policy is in a language version this core does not know. The renderers count such a
19
+ * policy as needing sign-in, the mildest gate
20
+ * to advertise, and locks it.
21
+ */
22
+ export declare function isUnknownPolicyVersion(policy: ClientDeliveryAssetAccessPolicy | null | undefined): boolean;
23
+ /**
24
+ * The access state to show on a card, from its policy and the viewer. Fail-closed, as the
25
+ * Client Delivery contract requires: a malformed policy is locked, and so is an unsupported
26
+ * version, except that a signed-out viewer is asked to sign in first. The
27
+ * state is for display; playback authorization stays with the server.
28
+ */
29
+ export declare function accessState(policy: ClientDeliveryAssetAccessPolicy | undefined, viewer: ViewerAccess): AccessState;
package/dist/access.js ADDED
@@ -0,0 +1,46 @@
1
+ /** The policy language version this core understands. */
2
+ const SUPPORTED_POLICY_VERSION = 1;
3
+ function isPolicy(policy) {
4
+ return (typeof policy === 'object' &&
5
+ policy !== null &&
6
+ typeof policy.authenticationRequired === 'boolean' &&
7
+ Array.isArray(policy.requiredEntitlements) &&
8
+ policy.requiredEntitlements.every((key) => typeof key === 'string') &&
9
+ (policy.match === 'any' || policy.match === 'all'));
10
+ }
11
+ /** Whether this core can read a policy: the supported version, well formed. */
12
+ export function isReadablePolicy(policy) {
13
+ return policy?.version === SUPPORTED_POLICY_VERSION && isPolicy(policy);
14
+ }
15
+ /**
16
+ * Whether a policy is in a language version this core does not know. The renderers count such a
17
+ * policy as needing sign-in, the mildest gate
18
+ * to advertise, and locks it.
19
+ */
20
+ export function isUnknownPolicyVersion(policy) {
21
+ return typeof policy === 'object' && policy !== null && policy.version !== SUPPORTED_POLICY_VERSION;
22
+ }
23
+ /**
24
+ * The access state to show on a card, from its policy and the viewer. Fail-closed, as the
25
+ * Client Delivery contract requires: a malformed policy is locked, and so is an unsupported
26
+ * version, except that a signed-out viewer is asked to sign in first. The
27
+ * state is for display; playback authorization stays with the server.
28
+ */
29
+ export function accessState(policy, viewer) {
30
+ if (policy === undefined)
31
+ return 'open';
32
+ if (isUnknownPolicyVersion(policy))
33
+ return viewer.signedIn ? 'locked' : 'sign-in';
34
+ if (!isReadablePolicy(policy))
35
+ return 'locked';
36
+ const required = policy.requiredEntitlements;
37
+ if (!viewer.signedIn && (policy.authenticationRequired || required.length > 0))
38
+ return 'sign-in';
39
+ if (required.length === 0)
40
+ return 'open';
41
+ if (viewer.entitlements === undefined)
42
+ return policy.failurePosture === 'open' ? 'open' : 'locked';
43
+ const held = new Set(viewer.entitlements);
44
+ const granted = policy.match === 'all' ? required.every((key) => held.has(key)) : required.some((key) => held.has(key));
45
+ return granted ? 'open' : 'locked';
46
+ }
@@ -0,0 +1,78 @@
1
+ import { type ViewerAccess } from './access.js';
2
+ import type { AccessBadgeKind, AccessBadgeModel, BadgeKind, BadgeModel, CardModel } from './cards.js';
3
+ export type BadgeCorner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
4
+ /** A badge's copy: fixed text, or a function of what it counts, such as a collection's videos. */
5
+ export type BadgeLabel = string | ((count: number) => string);
6
+ /** How a site shows one kind of badge: its copy, corner and style. */
7
+ export interface BadgePreset {
8
+ /**
9
+ * Copy in place of the core's, such as 'Upcoming' for 'UPCOMING'. A function words a
10
+ * counting badge, such as `(n) => n + " clips"` for a collection's video count.
11
+ */
12
+ label?: BadgeLabel;
13
+ corner?: BadgeCorner;
14
+ /** Emitted as `data-style`. The renderers style `neutral`, `strong` and `live`; a site styles its own. */
15
+ style?: string;
16
+ /** Adds the pulsing live indicator. */
17
+ pulse?: boolean;
18
+ /** Leaves this kind off cards. */
19
+ hidden?: boolean;
20
+ }
21
+ /** A site's badge vocabulary, by kind. Kinds it leaves out keep the defaults. */
22
+ export type BadgePresets = Partial<Record<BadgeKind, BadgePreset>>;
23
+ type ResolvedPreset = Required<Omit<BadgePreset, 'label'>> & {
24
+ label?: BadgeLabel;
25
+ };
26
+ export declare const DEFAULT_BADGE_PRESETS: Record<BadgeKind, ResolvedPreset>;
27
+ export interface ShownBadge {
28
+ badge: BadgeModel | AccessBadgeModel;
29
+ label: string;
30
+ style: string;
31
+ pulse: boolean;
32
+ }
33
+ export interface BadgeGroup {
34
+ corner: BadgeCorner;
35
+ badges: ShownBadge[];
36
+ }
37
+ /**
38
+ * How a site words its access badges. `sign-in` is for a signed-out viewer on a card that
39
+ * needs only sign-in; `entitlement` for a card that needs an entitlement the viewer does
40
+ * not hold. `entitlements` words one entitlement key in place of `entitlement`, and shows
41
+ * it even while `entitlement` stays hidden.
42
+ */
43
+ export interface AccessBadges {
44
+ 'sign-in'?: AccessBadgePreset;
45
+ entitlement?: AccessBadgePreset;
46
+ entitlements?: Record<string, AccessBadgePreset>;
47
+ }
48
+ /** An access badge's preset. Its copy is fixed text, as it counts nothing. */
49
+ export type AccessBadgePreset = Omit<BadgePreset, 'label'> & {
50
+ label?: string;
51
+ };
52
+ type ResolvedAccessPreset = Required<AccessBadgePreset>;
53
+ /** Sign-in shows by default; the entitlement badge is off until a site words it. */
54
+ export declare const DEFAULT_ACCESS_BADGES: Record<AccessBadgeKind, ResolvedAccessPreset>;
55
+ export interface ShownAccessBadge extends ShownBadge {
56
+ badge: AccessBadgeModel;
57
+ corner: BadgeCorner;
58
+ }
59
+ /**
60
+ * The access badge a card shows this viewer, if any. With no policy, none. A policy this
61
+ * core cannot read as version 1 counts as needing sign-in, the mildest gate to advertise,
62
+ * so a signed-out viewer sees the sign-in badge. A policy naming any entitlement never
63
+ * shows the sign-in badge, since signing in alone would not clear it; it shows the
64
+ * entitlement badge when the viewer lacks access. Otherwise a signed-out viewer sees the
65
+ * sign-in badge exactly when `authenticationRequired` is true, even if the rest of the
66
+ * policy is malformed. Without the viewer's state there is none: the badge is a hint, and
67
+ * playback authorization stays with the server. An entitlement badge names one the viewer
68
+ * is missing, never one they hold, as an `all` policy can leave them holding some.
69
+ */
70
+ export declare function accessBadge(card: CardModel, viewer: ViewerAccess | undefined, settings?: AccessBadges): ShownAccessBadge | undefined;
71
+ /**
72
+ * A card's badges, grouped by corner in a fixed order, after the site's presets. Within a
73
+ * corner, in this order: the event badge (LIVE or UPCOMING) first, then the card's
74
+ * other badges, then the access badge from accessBadge. Bottom corners stay empty while
75
+ * the card shows a progress bar, which runs along the bottom.
76
+ */
77
+ export declare function badgeGroups(card: CardModel, presets?: BadgePresets, access?: ShownAccessBadge): BadgeGroup[];
78
+ export {};
package/dist/badges.js ADDED
@@ -0,0 +1,89 @@
1
+ import { accessState, isReadablePolicy, isUnknownPolicyVersion } from './access.js';
2
+ export const DEFAULT_BADGE_PRESETS = {
3
+ new: { corner: 'top-left', style: 'strong', pulse: false, hidden: false },
4
+ 'new-season': { corner: 'top-left', style: 'strong', pulse: false, hidden: false },
5
+ 'new-episode': { corner: 'top-left', style: 'strong', pulse: false, hidden: false },
6
+ 'new-video': { corner: 'top-left', style: 'strong', pulse: false, hidden: false },
7
+ live: { corner: 'top-left', style: 'live', pulse: true, hidden: false },
8
+ upcoming: { corner: 'top-left', style: 'strong', pulse: false, hidden: false },
9
+ duration: { corner: 'bottom-right', style: 'neutral', pulse: false, hidden: false },
10
+ collection: { corner: 'bottom-right', style: 'neutral', pulse: false, hidden: false },
11
+ episodes: { corner: 'bottom-right', style: 'neutral', pulse: false, hidden: false },
12
+ label: { corner: 'top-left', style: 'neutral', pulse: false, hidden: false },
13
+ 'now-playing': { corner: 'top-left', style: 'strong', pulse: false, hidden: false },
14
+ };
15
+ const CORNERS = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
16
+ /** Sign-in shows by default; the entitlement badge is off until a site words it. */
17
+ export const DEFAULT_ACCESS_BADGES = {
18
+ 'sign-in': { label: 'Sign in to watch', corner: 'top-left', style: 'neutral', pulse: false, hidden: false },
19
+ entitlement: { label: 'Locked', corner: 'top-left', style: 'neutral', pulse: false, hidden: true },
20
+ };
21
+ /**
22
+ * The access badge a card shows this viewer, if any. With no policy, none. A policy this
23
+ * core cannot read as version 1 counts as needing sign-in, the mildest gate to advertise,
24
+ * so a signed-out viewer sees the sign-in badge. A policy naming any entitlement never
25
+ * shows the sign-in badge, since signing in alone would not clear it; it shows the
26
+ * entitlement badge when the viewer lacks access. Otherwise a signed-out viewer sees the
27
+ * sign-in badge exactly when `authenticationRequired` is true, even if the rest of the
28
+ * policy is malformed. Without the viewer's state there is none: the badge is a hint, and
29
+ * playback authorization stays with the server. An entitlement badge names one the viewer
30
+ * is missing, never one they hold, as an `all` policy can leave them holding some.
31
+ */
32
+ export function accessBadge(card, viewer, settings = {}) {
33
+ const policy = card.access;
34
+ if (!viewer || !policy)
35
+ return undefined;
36
+ const signIn = () => viewer.signedIn
37
+ ? undefined
38
+ : shownAccess({ kind: 'sign-in', label: '' }, { ...DEFAULT_ACCESS_BADGES['sign-in'], ...settings['sign-in'] });
39
+ if (typeof policy !== 'object' || isUnknownPolicyVersion(policy))
40
+ return signIn();
41
+ if (!namesEntitlements(policy))
42
+ return policy.authenticationRequired === true ? signIn() : undefined;
43
+ if (!isReadablePolicy(policy) || accessState(policy, viewer) === 'open')
44
+ return undefined;
45
+ const held = new Set(viewer.entitlements);
46
+ const missing = policy.requiredEntitlements.filter((key) => !held.has(key));
47
+ const entitlement = missing.find((key) => settings.entitlements?.[key]);
48
+ const preset = { ...DEFAULT_ACCESS_BADGES.entitlement, ...settings.entitlement };
49
+ const named = entitlement ? { ...preset, hidden: false, ...settings.entitlements[entitlement] } : preset;
50
+ return shownAccess({ kind: 'entitlement', label: '', ...(entitlement ? { entitlement } : {}) }, named);
51
+ }
52
+ /** Whether a policy names any entitlement, read as loosely as the wire may send it. */
53
+ function namesEntitlements(policy) {
54
+ const required = policy.requiredEntitlements;
55
+ return (Array.isArray(required) || typeof required === 'string') && required.length > 0;
56
+ }
57
+ function shownAccess(badge, preset) {
58
+ if (preset.hidden)
59
+ return undefined;
60
+ return { badge: { ...badge, label: preset.label }, label: preset.label, style: preset.style, pulse: preset.pulse, corner: preset.corner };
61
+ }
62
+ /**
63
+ * A card's badges, grouped by corner in a fixed order, after the site's presets. Within a
64
+ * corner, in this order: the event badge (LIVE or UPCOMING) first, then the card's
65
+ * other badges, then the access badge from accessBadge. Bottom corners stay empty while
66
+ * the card shows a progress bar, which runs along the bottom.
67
+ */
68
+ export function badgeGroups(card, presets = {}, access) {
69
+ const groups = new Map();
70
+ const show = (corner, badge) => {
71
+ if (card.progress !== undefined && corner.startsWith('bottom'))
72
+ return;
73
+ groups.set(corner, [...(groups.get(corner) ?? []), badge]);
74
+ };
75
+ for (const badge of card.badges) {
76
+ const preset = { ...DEFAULT_BADGE_PRESETS[badge.kind], ...presets[badge.kind] };
77
+ if (preset.hidden)
78
+ continue;
79
+ show(preset.corner, { badge, label: badgeLabel(badge, preset.label), style: preset.style, pulse: preset.pulse });
80
+ }
81
+ if (access)
82
+ show(access.corner, { badge: access.badge, label: access.label, style: access.style, pulse: access.pulse });
83
+ return CORNERS.filter((corner) => groups.has(corner)).map((corner) => ({ corner, badges: groups.get(corner) }));
84
+ }
85
+ function badgeLabel(badge, label) {
86
+ if (typeof label === 'function')
87
+ return badge.count === undefined ? badge.label : label(badge.count);
88
+ return label ?? badge.label;
89
+ }
@@ -0,0 +1,70 @@
1
+ import type { RecencyKind } from './recency.js';
2
+ export type CardKind = 'asset' | 'container' | 'product' | 'profile' | 'external-link' | 'custom' | 'schedule-event' | 'category' | 'unknown';
3
+ /**
4
+ * What a Continue Watching container card plays: the asset the API picked for the
5
+ * viewer's journey through the container, with the video assignment to play it with.
6
+ */
7
+ export interface PlayTarget {
8
+ assetId: string;
9
+ slug?: string;
10
+ videoAssignmentId: string;
11
+ seasonNumber?: number;
12
+ episodeNumber?: number;
13
+ /** Why the API picked this asset: not-started, resume or advance. */
14
+ reason: string;
15
+ /** Where to resume, when the viewer has progress on this asset. */
16
+ resumeFromSeconds?: number;
17
+ }
18
+ /** Where a card leads. Sites turn this into their own URL; the core never builds one. */
19
+ export type CardTarget = {
20
+ type: 'category';
21
+ id: string;
22
+ slug: string;
23
+ taxonomySlug: string;
24
+ } | {
25
+ type: 'asset' | 'product' | 'profile';
26
+ id: string;
27
+ slug?: string;
28
+ subtype?: string;
29
+ /** For assets placed with a specific video, the assignment to play. */
30
+ videoAssignmentId?: string;
31
+ /** On Continue Watching asset cards: where to resume, as the API selected it. */
32
+ resumeFromSeconds?: number;
33
+ } | {
34
+ type: 'container';
35
+ id: string;
36
+ slug?: string;
37
+ subtype?: string;
38
+ /** On Continue Watching cards: the asset to play, rather than the container's page. */
39
+ play?: PlayTarget;
40
+ } | {
41
+ /** A schedule occurrence. Play it through the schedule event playback endpoint, not the asset's. */
42
+ type: 'schedule-event';
43
+ id: string;
44
+ /** The asset behind the event, for display and detail pages. */
45
+ assetId: string;
46
+ /** The asset's subtype, such as `stream`. */
47
+ assetSubtype?: string;
48
+ } | {
49
+ type: 'external';
50
+ url: string;
51
+ };
52
+ /**
53
+ * `now-playing` marks the item a placed renderer's `currentId` names, such as a player page's
54
+ * video. `collection` counts a collection's videos and `episodes` a season's.
55
+ */
56
+ export type BadgeKind = RecencyKind | 'live' | 'upcoming' | 'duration' | 'collection' | 'episodes' | 'label' | 'now-playing';
57
+ export interface BadgeModel {
58
+ kind: BadgeKind;
59
+ label: string;
60
+ /** What a counting badge counts, such as a collection's videos or a season's episodes. A site's label function words it. */
61
+ count?: number;
62
+ }
63
+ /** A badge from the viewer's access to a card: sign in to watch, or an entitlement they lack. */
64
+ export type AccessBadgeKind = 'sign-in' | 'entitlement';
65
+ export interface AccessBadgeModel {
66
+ kind: AccessBadgeKind;
67
+ label: string;
68
+ /** For an entitlement badge, the key whose wording it took, when the site named one. */
69
+ entitlement?: string;
70
+ }
@@ -0,0 +1 @@
1
+ export {};