@aparte/plugin-cards 0.0.0-stage → 0.29.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/LICENSE +21 -0
- package/README.md +151 -2
- package/dist/carousel-DwgHvriV.js +63 -0
- package/dist/carousel-DwgHvriV.js.map +1 -0
- package/dist/controls-BYUm4aRV.js +62 -0
- package/dist/controls-BYUm4aRV.js.map +1 -0
- package/dist/format-BFq79ydr.js +728 -0
- package/dist/format-BFq79ydr.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -0
- package/dist/items/icons.d.ts +12 -0
- package/dist/items/icons.d.ts.map +1 -0
- package/dist/items/index.d.ts +100 -0
- package/dist/items/index.d.ts.map +1 -0
- package/dist/items.js +698 -0
- package/dist/items.js.map +1 -0
- package/dist/kit/carousel.d.ts +17 -0
- package/dist/kit/carousel.d.ts.map +1 -0
- package/dist/kit/controls.d.ts +51 -0
- package/dist/kit/controls.d.ts.map +1 -0
- package/dist/kit/define.d.ts +201 -0
- package/dist/kit/define.d.ts.map +1 -0
- package/dist/kit/dom.d.ts +40 -0
- package/dist/kit/dom.d.ts.map +1 -0
- package/dist/kit/fence.d.ts +39 -0
- package/dist/kit/fence.d.ts.map +1 -0
- package/dist/kit/format.d.ts +49 -0
- package/dist/kit/format.d.ts.map +1 -0
- package/dist/kit/rating.d.ts +19 -0
- package/dist/kit/rating.d.ts.map +1 -0
- package/dist/kit/sparkline.d.ts +19 -0
- package/dist/kit/sparkline.d.ts.map +1 -0
- package/dist/kit/strings.d.ts +139 -0
- package/dist/kit/strings.d.ts.map +1 -0
- package/dist/link/icons.d.ts +16 -0
- package/dist/link/icons.d.ts.map +1 -0
- package/dist/link/index.d.ts +98 -0
- package/dist/link/index.d.ts.map +1 -0
- package/dist/link.js +387 -0
- package/dist/link.js.map +1 -0
- package/dist/media/icons.d.ts +28 -0
- package/dist/media/icons.d.ts.map +1 -0
- package/dist/media/index.d.ts +107 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media.js +610 -0
- package/dist/media.js.map +1 -0
- package/dist/order/icons.d.ts +49 -0
- package/dist/order/icons.d.ts.map +1 -0
- package/dist/order/index.d.ts +204 -0
- package/dist/order/index.d.ts.map +1 -0
- package/dist/order.js +1843 -0
- package/dist/order.js.map +1 -0
- package/dist/place/icons.d.ts +18 -0
- package/dist/place/icons.d.ts.map +1 -0
- package/dist/place/index.d.ts +207 -0
- package/dist/place/index.d.ts.map +1 -0
- package/dist/place.js +1456 -0
- package/dist/place.js.map +1 -0
- package/dist/plan/icons.d.ts +25 -0
- package/dist/plan/icons.d.ts.map +1 -0
- package/dist/plan/index.d.ts +153 -0
- package/dist/plan/index.d.ts.map +1 -0
- package/dist/plan.js +1191 -0
- package/dist/plan.js.map +1 -0
- package/dist/product/icons.d.ts +16 -0
- package/dist/product/icons.d.ts.map +1 -0
- package/dist/product/index.d.ts +133 -0
- package/dist/product/index.d.ts.map +1 -0
- package/dist/product.js +1245 -0
- package/dist/product.js.map +1 -0
- package/dist/rating-CHVajQ2b.js +35 -0
- package/dist/rating-CHVajQ2b.js.map +1 -0
- package/dist/score/icons.d.ts +35 -0
- package/dist/score/icons.d.ts.map +1 -0
- package/dist/score/index.d.ts +185 -0
- package/dist/score/index.d.ts.map +1 -0
- package/dist/score.js +1843 -0
- package/dist/score.js.map +1 -0
- package/dist/sparkline-C-cA9obz.js +56 -0
- package/dist/sparkline-C-cA9obz.js.map +1 -0
- package/dist/stats/icons.d.ts +19 -0
- package/dist/stats/icons.d.ts.map +1 -0
- package/dist/stats/index.d.ts +137 -0
- package/dist/stats/index.d.ts.map +1 -0
- package/dist/stats.js +1085 -0
- package/dist/stats.js.map +1 -0
- package/dist/stock/chart.d.ts +135 -0
- package/dist/stock/chart.d.ts.map +1 -0
- package/dist/stock/icons.d.ts +15 -0
- package/dist/stock/icons.d.ts.map +1 -0
- package/dist/stock/index.d.ts +195 -0
- package/dist/stock/index.d.ts.map +1 -0
- package/dist/stock.js +1013 -0
- package/dist/stock.js.map +1 -0
- package/dist/weather/conditions.d.ts +25 -0
- package/dist/weather/conditions.d.ts.map +1 -0
- package/dist/weather/icons.d.ts +12 -0
- package/dist/weather/icons.d.ts.map +1 -0
- package/dist/weather/index.d.ts +146 -0
- package/dist/weather/index.d.ts.map +1 -0
- package/dist/weather.js +1162 -0
- package/dist/weather.js.map +1 -0
- package/package.json +137 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aparté
|
|
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,152 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @aparte/plugin-cards
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Rich cards for [aparté](https://apartejs.dev): a tool's result drawn as a card instead of a
|
|
4
|
+
row — weather, products, places, figures, orders, quotes, plans, scores, media, links — in variants, from your tool's own fields. Each card has its own entry,
|
|
5
|
+
so an app ships only the cards it imports.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @aparte/plugin-cards @aparte/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { setupWeatherCard } from '@aparte/plugin-cards/weather';
|
|
13
|
+
|
|
14
|
+
setupWeatherCard({
|
|
15
|
+
tool: 'get_weather',
|
|
16
|
+
map: (result) => ({ location: result.city, temperature: result.temp_c, condition: result.sky }),
|
|
17
|
+
variant: 'hero', // or 'compact', 'forecast', or a function of the data
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The full guide: [apartejs.dev/guides/cards](https://apartejs.dev/guides/cards/).
|
|
22
|
+
|
|
23
|
+
## The weather card — `@aparte/plugin-cards/weather`
|
|
24
|
+
|
|
25
|
+
- `setupWeatherCard(options)` plugs the card in and returns the function that unplugs it.
|
|
26
|
+
`options`: `tool`, `map`, `mapInput`, `variant`, `actions`, `onChange`, `config`.
|
|
27
|
+
- `weatherCard` is the card itself: `weatherCard.setup(options)`, and `weatherCard.segment(data)`
|
|
28
|
+
for a `card` segment with its fallback sentence.
|
|
29
|
+
- `readWeather(raw)` is the card's reading of its fields; `fromOpenMeteo(response, { location })`
|
|
30
|
+
reads an Open-Meteo forecast response.
|
|
31
|
+
- `conditionFromWmo`, `conditionFromOpenWeather` and `conditionFromText` turn the usual APIs' codes
|
|
32
|
+
into one of `WEATHER_CONDITIONS`.
|
|
33
|
+
- Types: `WeatherInput`, `WeatherData`, `WeatherHour`, `WeatherDay`, `WeatherCondition`,
|
|
34
|
+
`WeatherVariant`, `WindUnit`.
|
|
35
|
+
|
|
36
|
+
## The generic card — `@aparte/plugin-cards/items`
|
|
37
|
+
|
|
38
|
+
Articles, search results, anything with a title and a picture: `carousel` (default), `list`, `grid`.
|
|
39
|
+
|
|
40
|
+
- `setupItemsCard(options)`, `itemsCard` (`.setup`, `.segment`), `readItems(raw)`, `ITEMS_MAX` (10).
|
|
41
|
+
- Types: `ItemsInput`, `ItemInput`, `ItemImage`, `Item`, `ItemsData`, `ItemsVariant`.
|
|
42
|
+
|
|
43
|
+
## The product card — `@aparte/plugin-cards/product`
|
|
44
|
+
|
|
45
|
+
Prices, discount, rating, stock, merchant, options: `card` (default), `compact`, `detail`. Its reader takes
|
|
46
|
+
schema.org, Shopify, Google Shopping and Fake Store shapes, and prices written as text.
|
|
47
|
+
|
|
48
|
+
- `setupProductCard(options)`, `productCard` (`.setup`, `.segment`), `readProducts(raw)`, `discountOf(product)`.
|
|
49
|
+
- Types: `ProductsInput`, `ProductInput`, `Product`, `ProductData`, `ProductAvailability`, `ProductVariant`.
|
|
50
|
+
|
|
51
|
+
## The place card — `@aparte/plugin-cards/place`
|
|
52
|
+
|
|
53
|
+
Rating, price level, open now (computed from the hours, across midnight), distance, and Directions /
|
|
54
|
+
Call / Website buttons: `card` (default), `compact`, `detail` (with the week's hours). Its reader
|
|
55
|
+
takes both generations of Google Places and Yelp.
|
|
56
|
+
|
|
57
|
+
- `setupPlaceCard(options)` (adds `currencySymbol`, `now` and `builtIns`), `placeCard` (`.setup`, `.segment`),
|
|
58
|
+
`readPlaces(raw)`, `readHours(raw)`, `readTime(raw)`, `placeStatus(hours, date)`, `isOpenAt(hours, date)`,
|
|
59
|
+
`directionsUrl(place)`, `phoneUrl(phone)`.
|
|
60
|
+
- Types: `PlaceInput`, `Place`, `PlaceData`, `PlaceHoursInput`, `PlaceHoursRange`, `PlacePrice`,
|
|
61
|
+
`PlaceStatus`, `PlaceKind`, `PlaceVariant`, `PlaceCardOptions`.
|
|
62
|
+
|
|
63
|
+
## The key-figures card — `@aparte/plugin-cards/stats`
|
|
64
|
+
|
|
65
|
+
KPIs with their change and a sparkline: `tiles` (default), `list`, `hero`. A plain record
|
|
66
|
+
(`{ Revenue: 12000 }`) is read as label → value pairs.
|
|
67
|
+
|
|
68
|
+
- `setupStatsCard(options)`, `statsCard` (`.setup`, `.segment`), `readStats(raw)`, `statValue(config, stat)`,
|
|
69
|
+
`statChange(config, stat)`, `humanizeKey(key)`.
|
|
70
|
+
- Types: `StatsInput`, `StatInput`, `Stat`, `StatsData`, `StatFormat`, `StatIcon`, `StatsVariant`.
|
|
71
|
+
|
|
72
|
+
## The order card — `@aparte/plugin-cards/order`
|
|
73
|
+
|
|
74
|
+
An order or a booking as its receipt: `receipt` (default), `compact`, `confirmation`. Its reader
|
|
75
|
+
takes Shopify, WooCommerce and Stripe Checkout (`minorUnits: true` for amounts in cents).
|
|
76
|
+
|
|
77
|
+
- `setupOrderCard(options)`, `orderCard` (`.setup`, `.segment`), `readOrder(raw)`, `readWhen(value)`,
|
|
78
|
+
`nightsOf(from, to)`.
|
|
79
|
+
- Types: `OrderInput`, `OrderLineInput`, `OrderDateInput`, `OrderData`, `OrderLine`, `OrderFact`, `OrderWhen`,
|
|
80
|
+
`OrderKind`, `OrderStatus`, `OrderVariant`.
|
|
81
|
+
|
|
82
|
+
## The stock card — `@aparte/plugin-cards/stock`
|
|
83
|
+
|
|
84
|
+
A quote with its chart: `chart` (default), `compact`, `ticker`, `candles`. Range buttons over the
|
|
85
|
+
chart when the quote lists `ranges` and the card has an `onChange`. Its reader takes Yahoo Finance,
|
|
86
|
+
Alpha Vantage, Finnhub, CoinGecko and Binance.
|
|
87
|
+
|
|
88
|
+
- `setupStockCard(options)`, `stockCard` (`.setup`, `.segment`), `readQuotes(raw)`, `directionOf(quote)`,
|
|
89
|
+
`baselineOf(quote)`, `digitsOf(quote)`.
|
|
90
|
+
- Types: `QuotesInput`, `QuoteInput`, `Quote`, `StockData`, `ChartPoint`, `Candle`, `MarketState`,
|
|
91
|
+
`StockRange`, `StockVariant`.
|
|
92
|
+
|
|
93
|
+
## The plan card — `@aparte/plugin-cards/plan`
|
|
94
|
+
|
|
95
|
+
An agent's plan or task list, updated in place as it works: `checklist` (default), `timeline`,
|
|
96
|
+
`compact`. Its reader takes Markdown checklists and the todo lists agents already write.
|
|
97
|
+
|
|
98
|
+
- `setupPlanCard(options)`, `planCard` (`.setup`, `.segment`), `readPlan(raw)`, `readStatus(value)`,
|
|
99
|
+
`planProgress(data)`, `durationText(config, seconds)`, `PLAN_STEPS_SHOWN` (50).
|
|
100
|
+
- Types: `PlanInput`, `PlanStepInput`, `PlanStep`, `PlanData`, `PlanProgress`, `PlanStatus`, `PlanVariant`.
|
|
101
|
+
|
|
102
|
+
## The score card — `@aparte/plugin-cards/score`
|
|
103
|
+
|
|
104
|
+
A match or a scoreboard, any sport: `card` (default), `compact`, `detail`. Its reader takes
|
|
105
|
+
API-Football, football-data.org, ESPN and TheSportsDB.
|
|
106
|
+
|
|
107
|
+
- `setupScoreCard(options)` (adds `now`), `scoreCard` (`.setup`, `.segment`), `readMatches(raw)`,
|
|
108
|
+
`teamColor(value)`, `monogram(team)`, `inkOn(hex)`.
|
|
109
|
+
- Types: `ScoreInput`, `MatchInput`, `TeamInput`, `MatchPeriodInput`, `MatchEventInput`, `ScoreData`,
|
|
110
|
+
`Match`, `Team`, `MatchPeriod`, `MatchEvent`, `MatchStatus`, `MatchSide`, `MatchEventType`, `Sport`,
|
|
111
|
+
`ScoreVariant`, `ScoreCardOptions`.
|
|
112
|
+
|
|
113
|
+
## The media card — `@aparte/plugin-cards/media`
|
|
114
|
+
|
|
115
|
+
Pictures, videos and sounds, with a lightbox: `single` (default), `gallery`, `strip`. Native
|
|
116
|
+
`<video>` and `<audio>`; a YouTube or Vimeo page is a poster that links to it.
|
|
117
|
+
|
|
118
|
+
- `setupMediaCard(options)`, `mediaCard` (`.setup`, `.segment`), `readMedia(raw)`, `readDuration(value)`,
|
|
119
|
+
`formatDuration(config, seconds)`.
|
|
120
|
+
- Types: `MediumInput`, `Medium`, `MediaData`, `MediaKind`, `MediaVariant`, `MediaCardOptions`.
|
|
121
|
+
|
|
122
|
+
## The link card — `@aparte/plugin-cards/link`
|
|
123
|
+
|
|
124
|
+
A web page, previewed: `card` (default), `compact`, `quote`. Its reader takes Open Graph, Twitter
|
|
125
|
+
keys and the usual search APIs; the card fetches nothing.
|
|
126
|
+
|
|
127
|
+
- `setupLinkCard(options)`, `linkCard` (`.setup`, `.segment`), `readLinks(raw)`.
|
|
128
|
+
- Types: `LinkInput`, `Link`, `LinkData`, `LinkType`, `LinkVariant`, `LinkCardOptions`.
|
|
129
|
+
|
|
130
|
+
## The kit — `@aparte/plugin-cards`
|
|
131
|
+
|
|
132
|
+
What every card is made with, for a card of your own that behaves like ours (three sources,
|
|
133
|
+
states, variants, language switch, buttons):
|
|
134
|
+
|
|
135
|
+
- `setupCardFences(options)` (`CardFenceOptions`, `CARD_FENCE_PREFIX`): draws a ```` ```card-weather ````
|
|
136
|
+
block of JSON in an answer as its card, for backends that send only text; `load` fetches each
|
|
137
|
+
card the first time a block names it.
|
|
138
|
+
- `defineCard(definition)` and `CARD_SEGMENT_TYPE`; types `CardDefinition`, `CardSetupOptions`,
|
|
139
|
+
`CardContext`, `CardAction`, `CardActionEventDetail`, `CardChangeEventDetail`, `AparteCardSegment`.
|
|
140
|
+
- Controls, drawn only when the card has an `onChange`: `segmented` (`SegmentedOptions`), `toggle`
|
|
141
|
+
(`ToggleOptions`), `slider` (`SliderOptions`). A press fires `aparte-card-change`, and what `onChange`
|
|
142
|
+
returns is drawn in place.
|
|
143
|
+
- Drawing: `el`, `glyph`, `carousel` (`CarouselOptions`), `rating` (`RatingOptions`),
|
|
144
|
+
`sparkline` and `sparklinePath` (`SparklineOptions`).
|
|
145
|
+
- Reading: `safeUrl`, `webUrl`, `num`, `str`, `toDate`, `currencyCode`.
|
|
146
|
+
- Writing: `formatNumber`, `formatPercent`, `formatPrice`, `formatUnit`, `formatDay`, `formatHour`,
|
|
147
|
+
`formatWhen`; `word`, `languageOf`, `intlTag`, with `CardWord` and `CardLanguage`.
|
|
148
|
+
|
|
149
|
+
A card's words come in the ten languages of `@aparte/locales`, picked by the locale's `tag`; a key
|
|
150
|
+
of your locale wins (`aparteGlobalConfig.extendLocale({ cardsWeatherWind: 'Gusts' })`).
|
|
151
|
+
|
|
152
|
+
> ESM-only. Part of the aparté monorepo. MIT.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { e as el, r as word, h as formatNumber, K as KIT_WORDS, v as bindMarkup } from "./format-BFq79ydr.js";
|
|
2
|
+
function carousel(ctx, slides, { label } = {}) {
|
|
3
|
+
const track = el("div", { class: "aparte-rich-card__track", attrs: label ? { tabindex: 0 } : {} });
|
|
4
|
+
if (label) ctx.bind(track, label, "aria-label");
|
|
5
|
+
slides.forEach((slide, i) => {
|
|
6
|
+
const wrap = el("div", { class: "aparte-rich-card__slide", attrs: { role: "group", "aria-roledescription": "slide" } }, slide);
|
|
7
|
+
ctx.bind(wrap, () => word(ctx.config, "cardsSlide", KIT_WORDS.cardsSlide, { n: formatNumber(ctx.config, i + 1), total: formatNumber(ctx.config, slides.length) }), "aria-label");
|
|
8
|
+
track.append(wrap);
|
|
9
|
+
});
|
|
10
|
+
const button = (dir) => {
|
|
11
|
+
const b = el("button", {
|
|
12
|
+
class: `aparte-btn aparte-btn--icon aparte-btn--sm aparte-btn--surface aparte-rich-card__nav aparte-rich-card__nav--${dir < 0 ? "prev" : "next"}`,
|
|
13
|
+
attrs: { type: "button" }
|
|
14
|
+
});
|
|
15
|
+
bindMarkup(b, () => ctx.config.getIcon(dir < 0 ? "prevBranch" : "nextBranch"));
|
|
16
|
+
ctx.bind(b, () => word(ctx.config, dir < 0 ? "cardsPrevious" : "cardsNext", dir < 0 ? KIT_WORDS.cardsPrevious : KIT_WORDS.cardsNext), "aria-label");
|
|
17
|
+
b.addEventListener("click", () => step(dir));
|
|
18
|
+
return b;
|
|
19
|
+
};
|
|
20
|
+
const prev = button(-1);
|
|
21
|
+
const next = button(1);
|
|
22
|
+
const root = el("div", { class: "aparte-rich-card__carousel", attrs: label ? { role: "region", "aria-roledescription": "carousel" } : {} }, track, prev, next);
|
|
23
|
+
if (label) ctx.bind(root, label, "aria-label");
|
|
24
|
+
const rtl = () => typeof getComputedStyle === "function" && getComputedStyle(track).direction === "rtl";
|
|
25
|
+
function step(dir) {
|
|
26
|
+
const slide = track.firstElementChild;
|
|
27
|
+
const gap = parseFloat(getComputedStyle(track).columnGap) || 0;
|
|
28
|
+
const by = ((slide?.offsetWidth ?? track.clientWidth) + gap) * dir * (rtl() ? -1 : 1);
|
|
29
|
+
const still = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
30
|
+
track.scrollBy({ left: by, behavior: still ? "auto" : "smooth" });
|
|
31
|
+
}
|
|
32
|
+
function sync() {
|
|
33
|
+
const at = Math.abs(track.scrollLeft);
|
|
34
|
+
const room = track.scrollWidth - track.clientWidth;
|
|
35
|
+
root.toggleAttribute("data-overflow", room > 1);
|
|
36
|
+
const active = typeof document === "undefined" ? null : document.activeElement;
|
|
37
|
+
prev.disabled = at <= 1;
|
|
38
|
+
next.disabled = at >= room - 1;
|
|
39
|
+
if (active === prev && prev.disabled) (next.disabled ? track : next).focus({ preventScroll: true });
|
|
40
|
+
if (active === next && next.disabled) (prev.disabled ? track : prev).focus({ preventScroll: true });
|
|
41
|
+
}
|
|
42
|
+
let frame = 0;
|
|
43
|
+
track.addEventListener("scroll", () => {
|
|
44
|
+
cancelAnimationFrame(frame);
|
|
45
|
+
frame = requestAnimationFrame(sync);
|
|
46
|
+
}, { passive: true });
|
|
47
|
+
if (typeof ResizeObserver === "function") {
|
|
48
|
+
const watch = new ResizeObserver(() => {
|
|
49
|
+
if (!track.isConnected) {
|
|
50
|
+
watch.disconnect();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
sync();
|
|
54
|
+
});
|
|
55
|
+
watch.observe(track);
|
|
56
|
+
}
|
|
57
|
+
sync();
|
|
58
|
+
return root;
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
carousel as c
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=carousel-DwgHvriV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"carousel-DwgHvriV.js","sources":["../src/kit/carousel.ts"],"sourcesContent":["/**\n * A row of slides that scrolls sideways: products, places, articles.\n *\n * Native scrolling with snap points, so touch, trackpad, wheel and keyboard (the track is\n * focusable; arrows scroll it) all work as the platform does. Two buttons step a slide at\n * a time and switch off at each end; they are not drawn when everything fits. Announced as\n * a carousel, each slide as \"2 of 5\" (the WAI-ARIA carousel pattern, without auto-play:\n * nothing here moves on its own).\n */\nimport { bindMarkup, type CardContext } from './define.js';\nimport { el } from './dom.js';\nimport { formatNumber } from './format.js';\nimport { KIT_WORDS, word } from './strings.js';\n\nexport interface CarouselOptions {\n /** The carousel's accessible name (\"Products\"); its slides are counted inside it. */\n label?: () => string;\n}\n\n/** The slides in a scrolling row, with its two buttons. */\nexport function carousel(ctx: CardContext, slides: HTMLElement[], { label }: CarouselOptions = {}): HTMLElement {\n // Focusable so the arrow keys scroll it — and then named, since a focus stop must say what\n // it is. Without a label the two buttons are the keyboard's way through.\n const track = el('div', { class: 'aparte-rich-card__track', attrs: label ? { tabindex: 0 } : {} });\n if (label) ctx.bind(track, label, 'aria-label');\n slides.forEach((slide, i) => {\n const wrap = el('div', { class: 'aparte-rich-card__slide', attrs: { role: 'group', 'aria-roledescription': 'slide' } }, slide);\n ctx.bind(wrap, () => word(ctx.config, 'cardsSlide', KIT_WORDS.cardsSlide, { n: formatNumber(ctx.config, i + 1), total: formatNumber(ctx.config, slides.length) }), 'aria-label');\n track.append(wrap);\n });\n\n const button = (dir: -1 | 1) => {\n const b = el('button', {\n class: `aparte-btn aparte-btn--icon aparte-btn--sm aparte-btn--surface aparte-rich-card__nav aparte-rich-card__nav--${dir < 0 ? 'prev' : 'next'}`,\n attrs: { type: 'button' },\n });\n bindMarkup(b, () => ctx.config.getIcon(dir < 0 ? 'prevBranch' : 'nextBranch'));\n ctx.bind(b, () => word(ctx.config, dir < 0 ? 'cardsPrevious' : 'cardsNext', dir < 0 ? KIT_WORDS.cardsPrevious : KIT_WORDS.cardsNext), 'aria-label');\n b.addEventListener('click', () => step(dir));\n return b;\n };\n const prev = button(-1);\n const next = button(1);\n // A region is a landmark, and a landmark needs a name: without a label it is a plain box.\n const root = el('div', { class: 'aparte-rich-card__carousel', attrs: label ? { role: 'region', 'aria-roledescription': 'carousel' } : {} }, track, prev, next);\n if (label) ctx.bind(root, label, 'aria-label');\n\n const rtl = () => typeof getComputedStyle === 'function' && getComputedStyle(track).direction === 'rtl';\n function step(dir: -1 | 1): void {\n const slide = track.firstElementChild as HTMLElement | null;\n const gap = parseFloat(getComputedStyle(track).columnGap) || 0;\n const by = ((slide?.offsetWidth ?? track.clientWidth) + gap) * dir * (rtl() ? -1 : 1);\n const still = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;\n track.scrollBy({ left: by, behavior: still ? 'auto' : 'smooth' });\n }\n function sync(): void {\n // In a right-to-left page `scrollLeft` runs from 0 down to a negative number.\n const at = Math.abs(track.scrollLeft);\n const room = track.scrollWidth - track.clientWidth;\n root.toggleAttribute('data-overflow', room > 1);\n // A button switched off while it holds the focus would drop it on the page: the\n // focus moves to the other button (or the track) first.\n const active = typeof document === 'undefined' ? null : document.activeElement;\n prev.disabled = at <= 1;\n next.disabled = at >= room - 1;\n if (active === prev && prev.disabled) (next.disabled ? track : next).focus({ preventScroll: true });\n if (active === next && next.disabled) (prev.disabled ? track : prev).focus({ preventScroll: true });\n }\n let frame = 0;\n track.addEventListener('scroll', () => {\n cancelAnimationFrame(frame);\n frame = requestAnimationFrame(sync);\n }, { passive: true });\n if (typeof ResizeObserver === 'function') {\n // A card has no teardown hook: the observer lets itself go once the track has left\n // the page (a removal reports a size change).\n const watch = new ResizeObserver(() => {\n if (!track.isConnected) { watch.disconnect(); return; }\n sync();\n });\n watch.observe(track);\n }\n sync();\n return root;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { e as el } from "./format-BFq79ydr.js";
|
|
2
|
+
function segmented(ctx, { name, options, value, label }) {
|
|
3
|
+
if (!ctx.live || options.length === 0) return null;
|
|
4
|
+
const group = el("div", { class: "aparte-rich-card__segmented", attrs: { role: "group" } });
|
|
5
|
+
if (label) ctx.bind(group, label, "aria-label");
|
|
6
|
+
for (const option of options) {
|
|
7
|
+
const pressed = option.value === value;
|
|
8
|
+
const button = el("button", {
|
|
9
|
+
class: "aparte-rich-card__segment",
|
|
10
|
+
attrs: { type: "button", "aria-pressed": pressed ? "true" : "false", "data-card-key": `${name}:${option.value}` }
|
|
11
|
+
});
|
|
12
|
+
if (typeof option.label === "function") ctx.bind(button, option.label);
|
|
13
|
+
else button.textContent = option.label;
|
|
14
|
+
button.addEventListener("click", () => {
|
|
15
|
+
if (button.getAttribute("aria-pressed") === "true") return;
|
|
16
|
+
for (const other of group.querySelectorAll(".aparte-rich-card__segment")) other.setAttribute("aria-pressed", other === button ? "true" : "false");
|
|
17
|
+
ctx.change(name, option.value);
|
|
18
|
+
});
|
|
19
|
+
group.append(button);
|
|
20
|
+
}
|
|
21
|
+
return group;
|
|
22
|
+
}
|
|
23
|
+
function toggle(ctx, { name, checked, label }) {
|
|
24
|
+
if (!ctx.live) return null;
|
|
25
|
+
const button = el("button", {
|
|
26
|
+
class: "aparte-rich-card__toggle",
|
|
27
|
+
attrs: { type: "button", role: "switch", "aria-checked": checked ? "true" : "false", "data-card-key": name }
|
|
28
|
+
}, el("span", { class: "aparte-rich-card__toggle-thumb", attrs: { "aria-hidden": "true" } }));
|
|
29
|
+
ctx.bind(button, label, "aria-label");
|
|
30
|
+
button.addEventListener("click", () => {
|
|
31
|
+
const next = button.getAttribute("aria-checked") !== "true";
|
|
32
|
+
button.setAttribute("aria-checked", next ? "true" : "false");
|
|
33
|
+
ctx.change(name, next);
|
|
34
|
+
});
|
|
35
|
+
return button;
|
|
36
|
+
}
|
|
37
|
+
function slider(ctx, { name, value, min = 0, max = 100, step = 1, label, format = String }) {
|
|
38
|
+
if (!ctx.live) return null;
|
|
39
|
+
const input = el("input", {
|
|
40
|
+
class: "aparte-rich-card__slider-input",
|
|
41
|
+
attrs: { type: "range", min, max, step, value, "data-card-key": name }
|
|
42
|
+
});
|
|
43
|
+
const shown = el("output", { class: "aparte-rich-card__slider-value" });
|
|
44
|
+
const name_ = el("span", { class: "aparte-rich-card__slider-label" });
|
|
45
|
+
ctx.bind(name_, label);
|
|
46
|
+
ctx.bind(input, label, "aria-label");
|
|
47
|
+
const paint = (v) => {
|
|
48
|
+
shown.textContent = format(v);
|
|
49
|
+
input.setAttribute("aria-valuetext", format(v));
|
|
50
|
+
};
|
|
51
|
+
ctx.bind(shown, () => format(Number(input.value)));
|
|
52
|
+
paint(value);
|
|
53
|
+
input.addEventListener("input", () => paint(Number(input.value)));
|
|
54
|
+
input.addEventListener("change", () => ctx.change(name, Number(input.value)));
|
|
55
|
+
return el("label", { class: "aparte-rich-card__slider" }, name_, input, shown);
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
slider as a,
|
|
59
|
+
segmented as s,
|
|
60
|
+
toggle as t
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=controls-BYUm4aRV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controls-BYUm4aRV.js","sources":["../src/kit/controls.ts"],"sourcesContent":["/**\n * Controls a card can carry: a row of choices, a switch, a slider. Each reports through\n * `ctx.change(name, value)`, so the card fires `aparte-card-change` and the app's\n * `onChange` answers with new data, drawn in place — a stock chart's range, a light's\n * brightness.\n *\n * A control is drawn only when something will answer it (`ctx.live`): without an\n * `onChange`, a button that does nothing would be a promise the card cannot keep\n * (ratified decision #8). Each control carries a `data-card-key`, so a redraw puts the\n * focus back on the control that was pressed.\n */\nimport type { CardContext } from './define.js';\nimport { el } from './dom.js';\n\nexport interface SegmentedOptions {\n /** The control's name in the change (`range`). */\n name: string;\n /** The choices; a label is a string or a function read again on a language switch. */\n options: Array<{ value: string; label: string | (() => string) }>;\n /** The choice in force. */\n value?: string;\n /** The group's accessible name. */\n label?: () => string;\n}\n\n/** A row of choices, one pressed (`aria-pressed`). `null` when nothing would answer it. */\nexport function segmented(ctx: CardContext, { name, options, value, label }: SegmentedOptions): HTMLElement | null {\n if (!ctx.live || options.length === 0) return null;\n const group = el('div', { class: 'aparte-rich-card__segmented', attrs: { role: 'group' } });\n if (label) ctx.bind(group, label, 'aria-label');\n for (const option of options) {\n const pressed = option.value === value;\n const button = el('button', {\n class: 'aparte-rich-card__segment',\n attrs: { type: 'button', 'aria-pressed': pressed ? 'true' : 'false', 'data-card-key': `${name}:${option.value}` },\n });\n if (typeof option.label === 'function') ctx.bind(button, option.label);\n else button.textContent = option.label;\n button.addEventListener('click', () => {\n if (button.getAttribute('aria-pressed') === 'true') return;\n // Pressed at once, before the app answers: a second press reads what was chosen.\n for (const other of group.querySelectorAll('.aparte-rich-card__segment')) other.setAttribute('aria-pressed', other === button ? 'true' : 'false');\n ctx.change(name, option.value);\n });\n group.append(button);\n }\n return group;\n}\n\nexport interface ToggleOptions {\n name: string;\n checked: boolean;\n /** What it switches (\"Living-room light\"). */\n label: () => string;\n}\n\n/** A switch (`role=\"switch\"`). `null` when nothing would answer it. */\nexport function toggle(ctx: CardContext, { name, checked, label }: ToggleOptions): HTMLElement | null {\n if (!ctx.live) return null;\n const button = el('button', {\n class: 'aparte-rich-card__toggle',\n attrs: { type: 'button', role: 'switch', 'aria-checked': checked ? 'true' : 'false', 'data-card-key': name },\n }, el('span', { class: 'aparte-rich-card__toggle-thumb', attrs: { 'aria-hidden': 'true' } }));\n ctx.bind(button, label, 'aria-label');\n button.addEventListener('click', () => {\n // Flipped at once, before the app answers: a second click before the answer flips it\n // back, rather than sending the same state twice.\n const next = button.getAttribute('aria-checked') !== 'true';\n button.setAttribute('aria-checked', next ? 'true' : 'false');\n ctx.change(name, next);\n });\n return button;\n}\n\nexport interface SliderOptions {\n name: string;\n value: number;\n min?: number;\n max?: number;\n step?: number;\n label: () => string;\n /** How the value reads beside it, and to a screen reader (\"60 %\"). */\n format?: (value: number) => string;\n}\n\n/**\n * A slider: the value beside it follows while it moves, the change is reported once it is\n * let go (`change`, not `input`) — one request for a drag, not one per pixel.\n */\nexport function slider(ctx: CardContext, { name, value, min = 0, max = 100, step = 1, label, format = String }: SliderOptions): HTMLElement | null {\n if (!ctx.live) return null;\n const input = el('input', {\n class: 'aparte-rich-card__slider-input',\n attrs: { type: 'range', min, max, step, value, 'data-card-key': name },\n });\n const shown = el('output', { class: 'aparte-rich-card__slider-value' });\n const name_ = el('span', { class: 'aparte-rich-card__slider-label' });\n ctx.bind(name_, label);\n ctx.bind(input, label, 'aria-label');\n const paint = (v: number) => {\n shown.textContent = format(v);\n input.setAttribute('aria-valuetext', format(v));\n };\n ctx.bind(shown, () => format(Number(input.value)));\n paint(value);\n input.addEventListener('input', () => paint(Number(input.value)));\n input.addEventListener('change', () => ctx.change(name, Number(input.value)));\n return el('label', { class: 'aparte-rich-card__slider' }, name_, input, shown);\n}\n"],"names":[],"mappings":";AA0BO,SAAS,UAAU,KAAkB,EAAE,MAAM,SAAS,OAAO,SAA+C;AAC/G,MAAI,CAAC,IAAI,QAAQ,QAAQ,WAAW,EAAG,QAAO;AAC9C,QAAM,QAAQ,GAAG,OAAO,EAAE,OAAO,+BAA+B,OAAO,EAAE,MAAM,QAAA,GAAW;AAC1F,MAAI,MAAO,KAAI,KAAK,OAAO,OAAO,YAAY;AAC9C,aAAW,UAAU,SAAS;AAC1B,UAAM,UAAU,OAAO,UAAU;AACjC,UAAM,SAAS,GAAG,UAAU;AAAA,MACxB,OAAO;AAAA,MACP,OAAO,EAAE,MAAM,UAAU,gBAAgB,UAAU,SAAS,SAAS,iBAAiB,GAAG,IAAI,IAAI,OAAO,KAAK,GAAA;AAAA,IAAG,CACnH;AACD,QAAI,OAAO,OAAO,UAAU,gBAAgB,KAAK,QAAQ,OAAO,KAAK;AAAA,QAChE,QAAO,cAAc,OAAO;AACjC,WAAO,iBAAiB,SAAS,MAAM;AACnC,UAAI,OAAO,aAAa,cAAc,MAAM,OAAQ;AAEpD,iBAAW,SAAS,MAAM,iBAAiB,4BAA4B,EAAG,OAAM,aAAa,gBAAgB,UAAU,SAAS,SAAS,OAAO;AAChJ,UAAI,OAAO,MAAM,OAAO,KAAK;AAAA,IACjC,CAAC;AACD,UAAM,OAAO,MAAM;AAAA,EACvB;AACA,SAAO;AACX;AAUO,SAAS,OAAO,KAAkB,EAAE,MAAM,SAAS,SAA4C;AAClG,MAAI,CAAC,IAAI,KAAM,QAAO;AACtB,QAAM,SAAS,GAAG,UAAU;AAAA,IACxB,OAAO;AAAA,IACP,OAAO,EAAE,MAAM,UAAU,MAAM,UAAU,gBAAgB,UAAU,SAAS,SAAS,iBAAiB,KAAA;AAAA,EAAK,GAC5G,GAAG,QAAQ,EAAE,OAAO,kCAAkC,OAAO,EAAE,eAAe,OAAA,EAAO,CAAG,CAAC;AAC5F,MAAI,KAAK,QAAQ,OAAO,YAAY;AACpC,SAAO,iBAAiB,SAAS,MAAM;AAGnC,UAAM,OAAO,OAAO,aAAa,cAAc,MAAM;AACrD,WAAO,aAAa,gBAAgB,OAAO,SAAS,OAAO;AAC3D,QAAI,OAAO,MAAM,IAAI;AAAA,EACzB,CAAC;AACD,SAAO;AACX;AAiBO,SAAS,OAAO,KAAkB,EAAE,MAAM,OAAO,MAAM,GAAG,MAAM,KAAK,OAAO,GAAG,OAAO,SAAS,UAA6C;AAC/I,MAAI,CAAC,IAAI,KAAM,QAAO;AACtB,QAAM,QAAQ,GAAG,SAAS;AAAA,IACtB,OAAO;AAAA,IACP,OAAO,EAAE,MAAM,SAAS,KAAK,KAAK,MAAM,OAAO,iBAAiB,KAAA;AAAA,EAAK,CACxE;AACD,QAAM,QAAQ,GAAG,UAAU,EAAE,OAAO,kCAAkC;AACtE,QAAM,QAAQ,GAAG,QAAQ,EAAE,OAAO,kCAAkC;AACpE,MAAI,KAAK,OAAO,KAAK;AACrB,MAAI,KAAK,OAAO,OAAO,YAAY;AACnC,QAAM,QAAQ,CAAC,MAAc;AACzB,UAAM,cAAc,OAAO,CAAC;AAC5B,UAAM,aAAa,kBAAkB,OAAO,CAAC,CAAC;AAAA,EAClD;AACA,MAAI,KAAK,OAAO,MAAM,OAAO,OAAO,MAAM,KAAK,CAAC,CAAC;AACjD,QAAM,KAAK;AACX,QAAM,iBAAiB,SAAS,MAAM,MAAM,OAAO,MAAM,KAAK,CAAC,CAAC;AAChE,QAAM,iBAAiB,UAAU,MAAM,IAAI,OAAO,MAAM,OAAO,MAAM,KAAK,CAAC,CAAC;AAC5E,SAAO,GAAG,SAAS,EAAE,OAAO,8BAA8B,OAAO,OAAO,KAAK;AACjF;"}
|