@sequencestudio/web-core 0.0.0-stage → 0.5.0
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/LICENSE +21 -0
- package/README.md +183 -2
- package/dist/access.d.ts +29 -0
- package/dist/access.js +46 -0
- package/dist/badges.d.ts +78 -0
- package/dist/badges.js +89 -0
- package/dist/card-model.d.ts +70 -0
- package/dist/card-model.js +1 -0
- package/dist/cards.d.ts +141 -0
- package/dist/cards.js +383 -0
- package/dist/class-names.d.ts +63 -0
- package/dist/class-names.js +62 -0
- package/dist/clicks.d.ts +33 -0
- package/dist/clicks.js +12 -0
- package/dist/component-drag.d.ts +27 -0
- package/dist/component-drag.js +225 -0
- package/dist/component-items-route.d.ts +65 -0
- package/dist/component-items-route.js +80 -0
- package/dist/component-loading.d.ts +273 -0
- package/dist/component-loading.js +367 -0
- package/dist/detail-header.d.ts +102 -0
- package/dist/detail-header.js +178 -0
- package/dist/events.d.ts +50 -0
- package/dist/events.js +76 -0
- package/dist/game.d.ts +49 -0
- package/dist/game.js +62 -0
- package/dist/hero.d.ts +45 -0
- package/dist/hero.js +89 -0
- package/dist/images.d.ts +42 -0
- package/dist/images.js +81 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +25 -0
- package/dist/layout.d.ts +166 -0
- package/dist/layout.js +164 -0
- package/dist/loading.d.ts +23 -0
- package/dist/loading.js +25 -0
- package/dist/placed-items-route.d.ts +96 -0
- package/dist/placed-items-route.js +120 -0
- package/dist/placed-loading.d.ts +153 -0
- package/dist/placed-loading.js +214 -0
- package/dist/placed.d.ts +55 -0
- package/dist/placed.js +76 -0
- package/dist/player/action-feedback.d.ts +54 -0
- package/dist/player/action-feedback.js +161 -0
- package/dist/player/annotations.d.ts +46 -0
- package/dist/player/annotations.js +101 -0
- package/dist/player/chrome.d.ts +95 -0
- package/dist/player/chrome.js +222 -0
- package/dist/player/controls.d.ts +33 -0
- package/dist/player/controls.js +24 -0
- package/dist/player/delivery-type.d.ts +11 -0
- package/dist/player/delivery-type.js +14 -0
- package/dist/player/format-playback-time.d.ts +1 -0
- package/dist/player/format-playback-time.js +12 -0
- package/dist/player/fullscreen.d.ts +23 -0
- package/dist/player/fullscreen.js +136 -0
- package/dist/player/index.d.ts +36 -0
- package/dist/player/index.js +36 -0
- package/dist/player/live-recovery.d.ts +58 -0
- package/dist/player/live-recovery.js +96 -0
- package/dist/player/live.d.ts +22 -0
- package/dist/player/live.js +24 -0
- package/dist/player/markers.d.ts +42 -0
- package/dist/player/markers.js +74 -0
- package/dist/player/playback-analytics.d.ts +63 -0
- package/dist/player/playback-analytics.js +354 -0
- package/dist/player/playback-authorization.d.ts +23 -0
- package/dist/player/playback-authorization.js +80 -0
- package/dist/player/playback-quality.d.ts +28 -0
- package/dist/player/playback-quality.js +51 -0
- package/dist/player/player-keyboard.d.ts +29 -0
- package/dist/player/player-keyboard.js +220 -0
- package/dist/player/player-settings.d.ts +22 -0
- package/dist/player/player-settings.js +1 -0
- package/dist/player/progress-reporter-state.d.ts +108 -0
- package/dist/player/progress-reporter-state.js +71 -0
- package/dist/player/progress-reporter.d.ts +20 -0
- package/dist/player/progress-reporter.js +428 -0
- package/dist/player/progress-retry.d.ts +8 -0
- package/dist/player/progress-retry.js +23 -0
- package/dist/player/random-id.d.ts +14 -0
- package/dist/player/random-id.js +24 -0
- package/dist/player/ref.d.ts +8 -0
- package/dist/player/ref.js +1 -0
- package/dist/player/scrub-view.d.ts +51 -0
- package/dist/player/scrub-view.js +59 -0
- package/dist/player/seek.d.ts +5 -0
- package/dist/player/seek.js +17 -0
- package/dist/player/shaka-error.d.ts +70 -0
- package/dist/player/shaka-error.js +257 -0
- package/dist/player/shaka-playback.d.ts +65 -0
- package/dist/player/shaka-playback.js +640 -0
- package/dist/player/shaka-player-support.d.ts +119 -0
- package/dist/player/shaka-player-support.js +142 -0
- package/dist/player/shaka-text-tracks.d.ts +10 -0
- package/dist/player/shaka-text-tracks.js +35 -0
- package/dist/player/stall-tracker.d.ts +41 -0
- package/dist/player/stall-tracker.js +58 -0
- package/dist/player/styles.css +1678 -0
- package/dist/player/telemetry.d.ts +245 -0
- package/dist/player/telemetry.js +11 -0
- package/dist/player/test-ids.d.ts +91 -0
- package/dist/player/test-ids.js +64 -0
- package/dist/player/track-language.d.ts +22 -0
- package/dist/player/track-language.js +25 -0
- package/dist/player/track-options.d.ts +25 -0
- package/dist/player/track-options.js +106 -0
- package/dist/player/track-preference.d.ts +24 -0
- package/dist/player/track-preference.js +59 -0
- package/dist/player/track-settings.d.ts +61 -0
- package/dist/player/track-settings.js +305 -0
- package/dist/player/video-store.d.ts +28 -0
- package/dist/player/video-store.js +89 -0
- package/dist/player/viewer-seek.d.ts +18 -0
- package/dist/player/viewer-seek.js +25 -0
- package/dist/player/volume-preference.d.ts +20 -0
- package/dist/player/volume-preference.js +65 -0
- package/dist/presentation.d.ts +112 -0
- package/dist/presentation.js +125 -0
- package/dist/product.d.ts +31 -0
- package/dist/product.js +60 -0
- package/dist/recency.d.ts +46 -0
- package/dist/recency.js +156 -0
- package/dist/renderers.d.ts +64 -0
- package/dist/renderers.js +58 -0
- package/dist/revive.d.ts +17 -0
- package/dist/revive.js +64 -0
- package/dist/route-support.d.ts +72 -0
- package/dist/route-support.js +129 -0
- package/dist/server-browser.d.ts +1 -0
- package/dist/server-browser.js +4 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +4 -0
- package/dist/shelf-layout.d.ts +88 -0
- package/dist/shelf-layout.js +96 -0
- package/dist/test-ids.d.ts +159 -0
- package/dist/test-ids.js +170 -0
- package/dist/theme-config.d.ts +32 -0
- package/dist/theme-config.js +63 -0
- package/dist/tokens.d.ts +140 -0
- package/dist/tokens.js +224 -0
- 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
|
-
#
|
|
1
|
+
# @sequencestudio/web-core
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
package/dist/access.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/badges.d.ts
ADDED
|
@@ -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 {};
|