@stealthscale/component-surfaces 0.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stealth Scale B.V.
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 ADDED
@@ -0,0 +1,150 @@
1
+ # @stealthscale/component-surfaces
2
+
3
+ Renders the surfaces a page is built from.
4
+
5
+ Every value a theme can change is an axis of a component's recipe, so set it as a prop and write no
6
+ style. Change the element a component renders with `as`. A component with parts is published as a
7
+ namespace, `Card.Root`.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @stealthscale/component-surfaces
13
+ ```
14
+
15
+ The package peers on `react` and `@stealthscale/theme`. List the preset under `./theme` among the
16
+ presets your compiler installs.
17
+
18
+ ## Card
19
+
20
+ A card stacks a picture, a header, content or sections, and a footer of controls on one panel.
21
+
22
+ ```tsx
23
+ import { Button, IconButton } from "@stealthscale/component-actions";
24
+ import { Card } from "@stealthscale/component-surfaces";
25
+ import { EllipsisIcon, ReceiptTextIcon } from "lucide-react";
26
+
27
+ <Card.Root aria-labelledby="invoice-4821">
28
+ <Card.Header>
29
+ <Card.Indicator>
30
+ <ReceiptTextIcon aria-hidden />
31
+ </Card.Indicator>
32
+ <Card.Title id="invoice-4821">Invoice 4821</Card.Title>
33
+ <Card.Description>Issued on 2 September</Card.Description>
34
+ <Card.Aside>
35
+ <IconButton aria-label="More actions for invoice 4821" size="sm" variant="ghost">
36
+ <EllipsisIcon aria-hidden />
37
+ </IconButton>
38
+ </Card.Aside>
39
+ </Card.Header>
40
+ <Card.Content>Three lines, one unbilled.</Card.Content>
41
+ <Card.Footer>
42
+ <Button size="sm">Send</Button>
43
+ </Card.Footer>
44
+ </Card.Root>;
45
+ ```
46
+
47
+ | Part | Element | What it renders |
48
+ | ------------- | --------- | ------------------------------------------------------------------------- |
49
+ | `Root` | `article` | The panel. It resolves every variant |
50
+ | `Media` | `div` | A picture that extends to the card's edges |
51
+ | `Overlay` | `div` | A layer over the picture, inside `Media`, for a badge or a caption |
52
+ | `Header` | `div` | A three-column grid: indicator, title over description, aside |
53
+ | `Indicator` | `div` | An icon or an avatar image at the start of the header |
54
+ | `Title` | `h3` | The card's heading |
55
+ | `Description` | `p` | The line under the title |
56
+ | `Aside` | `div` | A control or a badge at the end of the header |
57
+ | `Content` | `div` | The body, which takes the height the other bands leave |
58
+ | `Section` | `div` | A band that extends to the side edges, with a rule to each band beside it |
59
+ | `Footer` | `div` | The controls |
60
+
61
+ | Axis | Values | Default |
62
+ | ------------- | ---------------------------------------------------------------------------------- | ---------- |
63
+ | `variant` | `elevated`, `outline`, `subtle`, `glass`, `plain` | `elevated` |
64
+ | `palette` | `primary`, `secondary`, `accent`, `neutral`, `info`, `success`, `warning`, `error` | none |
65
+ | `size` | `sm`, `md`, `lg`, `xl` | `md` |
66
+ | `radius` | the theme's corner radii | `l2` |
67
+ | `orientation` | `vertical`, `horizontal` | `vertical` |
68
+ | `scrim` | `true` | off |
69
+ | `divided` | `true` | off |
70
+ | `justify` | `start`, `center`, `end`, `between`, `around`, `evenly` | `end` |
71
+ | `interactive` | `true` | off |
72
+ | `disabled` | `true` | off |
73
+ | `effect` | `glow` | none |
74
+ | `motion` | `fade`, `rise`, `reveal` | none |
75
+
76
+ ### Name the card
77
+
78
+ Point `aria-labelledby` at the title's `id`, or pass `aria-label`. Pass `as="div"` for a card that
79
+ is part of its surroundings and does not need a name.
80
+
81
+ ### Header
82
+
83
+ Put the indicator, the title, the description and the aside directly in `Card.Header`. The indicator
84
+ and the aside are centred on the title and description rows. An `img` in the indicator renders as a
85
+ round avatar of 32 to 56px by size, and an `svg` placed directly in it takes the icon size of the
86
+ card's size.
87
+
88
+ ### Pictures
89
+
90
+ Put the picture in `Card.Media` with its alternative text, or `alt=""` when it is decorative. In a
91
+ vertical card the picture extends to both side edges, to the top edge as the first band, and to the
92
+ bottom edge as the last. In a horizontal card it covers the leading third at the card's full height.
93
+
94
+ Put a badge or a caption over the picture in `Card.Overlay`, after the image. Set `scrim` on the
95
+ root when the overlay contains text. It darkens the lower half of the picture and renders the
96
+ overlay in the dark color scheme.
97
+
98
+ ```tsx
99
+ <Card.Root scrim>
100
+ <Card.Media>
101
+ <img alt="" src={cover} />
102
+ <Card.Overlay>
103
+ <Badge variant="solid">Engineering</Badge>
104
+ <Text size="sm">6 min read</Text>
105
+ </Card.Overlay>
106
+ </Card.Media>
107
+ <Card.Header>
108
+ <Card.Title>How we settle payouts in two days</Card.Title>
109
+ </Card.Header>
110
+ </Card.Root>
111
+ ```
112
+
113
+ ### Sections
114
+
115
+ Use `Card.Section` for rows of settings, a list of features or a summary. A section extends to the
116
+ card's side edges and keeps its content at the card's inset, so its text is aligned with the header.
117
+ It renders a hairline between itself and each band beside it, and two adjacent sections share one
118
+ rule. Each rule spans the card's full width with the inset above and below it. `divided` renders the
119
+ rules under the header and above the footer the same way.
120
+
121
+ ### Interactive cards
122
+
123
+ Set `interactive` and put a link in the title. The link's hit area covers the whole card, and the
124
+ link keeps the focus and the accessible name. Other links and buttons in the card take their own
125
+ clicks. Do not put a click handler or a `tabindex` on the root, and do not put a second link in the
126
+ title.
127
+
128
+ ```tsx
129
+ <Card.Root interactive>
130
+ <Card.Header>
131
+ <Card.Title>
132
+ <Link href="/invoices/4821">Invoice 4821</Link>
133
+ </Card.Title>
134
+ </Card.Header>
135
+ </Card.Root>
136
+ ```
137
+
138
+ Set `disabled` to render the card at the disabled opacity with no pointer events. The root sets
139
+ `aria-disabled`. The styles do not remove the title's link from the tab order, so render the title
140
+ without the link, or give the link `aria-disabled` and no `href`.
141
+
142
+ ### Palettes
143
+
144
+ `palette` sets the palette of the focus ring and the glow. An `outline` card also sets its border to
145
+ the palette, and a `subtle` card fills with the palette's subtle color. The stylesheet contains
146
+ every palette whether a page uses it, so a value set at runtime has a class.
147
+
148
+ ## Licence
149
+
150
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1 @@
1
+ {"$schema":"http://cyclonedx.org/schema/bom-1.7.schema.json","bomFormat":"CycloneDX","specVersion":"1.7","version":1,"metadata":{"lifecycles":[{"phase":"build"}],"tools":{"components":[{"type":"application","name":"rolldown","version":"1.2.11"},{"type":"application","name":"vite","version":"8.3.1"},{"type":"library","name":"core","group":"@arethetypeswrong","version":"0.18.5","bom-ref":"pkg:npm/%40arethetypeswrong/core@0.18.5","author":"Andrew Branch","description":"The API behind arethetypeswrong.github.io","hashes":[{"alg":"SHA-512","content":"f72b63cc6c318e6f54cfb23d6af7dbb79be542deae93db914444b3263aabce797a58abc8e9dc184e8faf277534d96aeafe64778aa97f3f386f1e129d2480230d"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40arethetypeswrong/core@0.18.5","externalReferences":[{"url":"https://github.com/arethetypeswrong/arethetypeswrong.github.io.git#packages/core","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=20"}]},{"type":"library","name":"component-actions","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/component-actions@0.1.0","description":"Renders what a person presses: a button, a toggle group, a swap, a color mode toggle, a clipboard and a download trigger.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/component-actions@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#components/actions","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/components/actions#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"component-navigation","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/component-navigation@0.1.0","description":"Draws the ways a person moves between places: links, tabs, breadcrumbs and rails.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/component-navigation@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#components/navigation","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/components/navigation#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"theme","group":"@stealthscale","version":"0.3.0","bom-ref":"pkg:npm/%40stealthscale/theme@0.3.0","description":"Publishes the foundation every recipe is written against, the runtime every component binds its recipe with, and the vocabulary a theme is written in.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/theme@0.3.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#foundations/theme","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/foundations/theme#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config-i18n","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/vite-config-i18n@0.1.0","description":"Configures a package or an application with words: the catalogue plugin, and the words in scope for its specifications.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config-i18n@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-config-i18n","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-config-i18n#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config-react","group":"@stealthscale","version":"0.8.0","bom-ref":"pkg:npm/%40stealthscale/vite-config-react@0.8.0","description":"Adds the JSX transform, the React lint rules, a test document and an MDX compiler to a package that renders.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config-react@0.8.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-config-react","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-config-react#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config-specimen","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/vite-config-specimen@0.1.0","description":"Configures an application that shows a catalogue, and the workspace root the specimens sit under.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config-specimen@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-config-specimen","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-config-specimen#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config-theme","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/vite-config-theme@0.1.0","description":"Adds the runtime generator to a design-system package and the stylesheet compiler to an application.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config-theme@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-config-theme","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-config-theme#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config-typescript","group":"@stealthscale","version":"0.3.0","bom-ref":"pkg:npm/%40stealthscale/vite-config-typescript@0.3.0","description":"Sets the TypeScript options every stealthscale package compiles with.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config-typescript@0.3.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-config-typescript","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-config-typescript#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config","group":"@stealthscale","version":"0.6.0","bom-ref":"pkg:npm/%40stealthscale/vite-config@0.6.0","description":"Configures a package from five Vite+ tiers and a named layer for every block.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config@0.6.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-config","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-config#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"react","group":"@testing-library","version":"16.3.3","bom-ref":"pkg:npm/%40testing-library/react@16.3.3","author":"Kent C. Dodds <me@kentcdodds.com> (https://kentcdodds.com)","description":"Simple and complete React DOM testing utilities that encourage good testing practices.","hashes":[{"alg":"SHA-512","content":"528d7ddcd8106cf333ea5aeb86d45040570c0ba45efc42cb15b6ee54bdf4583959c65a597fdfe53074c05713d12f0d6ad62bbd389991d5cd812e210d46cb5d06"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40testing-library/react@16.3.3","externalReferences":[{"url":"https://github.com/testing-library/react-testing-library/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs.url\""},{"url":"https://github.com/testing-library/react-testing-library","type":"vcs","comment":"as detected from PackageJson property \"repository.url\""},{"url":"https://github.com/testing-library/react-testing-library#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=18"}]},{"type":"library","name":"react-dom","group":"@types","version":"19.3.0","bom-ref":"pkg:npm/%40types/react-dom@19.3.0","description":"TypeScript definitions for react-dom","hashes":[{"alg":"SHA-512","content":"648edb538da66575ca1e7fea34b130d88adb888354ed7e7ebdf81d8b10479023696165e32a07d0fcffaec866f90a338b07d51c9d37a0debca542d5768729b8e1"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40types/react-dom@19.3.0","externalReferences":[{"url":"https://github.com/DefinitelyTyped/DefinitelyTyped.git#types/react-dom","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react-dom","type":"website","comment":"as detected from PackageJson property \"homepage\""}]},{"type":"library","name":"react","group":"@types","version":"19.3.0","bom-ref":"pkg:npm/%40types/react@19.3.0","description":"TypeScript definitions for react","hashes":[{"alg":"SHA-512","content":"374ac50ae1fd628c46f7f9bad65f4c7e924a7e62ce554d1e9bb8a9233e9346049292f45e2c1f6f2fce4607ecabf01b3995eaa9be0f7a252c05e194b5b38be242"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40types/react@19.3.0","externalReferences":[{"url":"https://github.com/DefinitelyTyped/DefinitelyTyped.git#types/react","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react","type":"website","comment":"as detected from PackageJson property \"homepage\""}]},{"type":"library","name":"happy-dom","version":"20.14.5","bom-ref":"pkg:npm/happy-dom@20.14.5","author":"David Ortner","description":"Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG DOM and HTML.","hashes":[{"alg":"SHA-512","content":"c7f47392958ee346d38c8a13df4890b6deb814b2e61ff89171408dd97fff6cbc7b1f789f91d7cf5f2aacae8fce62dab32008622cc300ee69a2391f42dcd38a8d"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/happy-dom@20.14.5","externalReferences":[{"url":"https://github.com/capricorn86/happy-dom","type":"vcs","comment":"as detected from PackageJson property \"repository\""},{"url":"https://github.com/capricorn86/happy-dom","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=20.0.0"}]},{"type":"library","name":"lucide-react","version":"1.48.0","bom-ref":"pkg:npm/lucide-react@1.48.0","author":"Eric Fennis","description":"A Lucide icon library package for React applications.","hashes":[{"alg":"SHA-512","content":"474088298fdf5e20bacbdc510400e0b0af955b6a7fa5c4c938c84b65fd53fae1bebc952fc94474da718e8262080bb30f92234ae0ec7c4039dba85f2b25858f65"}],"licenses":[{"license":{"id":"ISC"}}],"purl":"pkg:npm/lucide-react@1.48.0","externalReferences":[{"url":"https://github.com/lucide-icons/lucide/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"https://github.com/lucide-icons/lucide.git#packages/lucide-react","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://lucide.dev","type":"website","comment":"as detected from PackageJson property \"homepage\""}]},{"type":"library","name":"oxc-transform-react","version":"0.151.0","bom-ref":"pkg:npm/oxc-transform-react@0.151.0","author":"Boshen and oxc contributors","description":"Oxc React Compiler Node API","hashes":[{"alg":"SHA-512","content":"60ed7f62cdc5202fee060ca37b1bc6b084d05085c318d9b1272eafa906ddf1663b263ccc0f5faf095879f65cdaf7f3f4dbf4d135a517c94eceaf6c70c7f29b77"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/oxc-transform-react@0.151.0","externalReferences":[{"url":"https://github.com/oxc-project/oxc/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/oxc-project/oxc.git#napi/transform-react","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://oxc.rs","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":"^20.19.0 || >=22.12.0"}]},{"type":"library","name":"react-dom","version":"19.3.0","bom-ref":"pkg:npm/react-dom@19.3.0","description":"React package for working with the DOM.","hashes":[{"alg":"SHA-512","content":"24393c76089fe753a31680c4ef4f8e4fd202c98afeebd1e59a284dc29d7e36c7db9dadede6c22209af59264b43990e3fd5bfeb9f6ea1200476b985c332be6bd1"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/react-dom@19.3.0","externalReferences":[{"url":"https://github.com/react/react/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs.url\""},{"url":"https://github.com/react/react.git#packages/react-dom","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://react.dev/","type":"website","comment":"as detected from PackageJson property \"homepage\""}]},{"type":"library","name":"react","version":"19.3.0","bom-ref":"pkg:npm/react@19.3.0","description":"React is a JavaScript library for building user interfaces.","hashes":[{"alg":"SHA-512","content":"13c2d471bb415addb46db9766281dfc7864305dc554df38ab42667f5c0d227897afe7ba8029729201723e3bb76c19a155fc836647b8c1db892860091d53e52a2"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/react@19.3.0","externalReferences":[{"url":"https://github.com/react/react/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"https://github.com/react/react.git#packages/react","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://react.dev/","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=0.10.0"}]},{"type":"library","name":"typescript","version":"7.0.2","bom-ref":"pkg:npm/typescript@7.0.2","author":"Microsoft Corp.","description":"TypeScript is a language for application scale JavaScript development","hashes":[{"alg":"SHA-512","content":"f0561abbdea8dcd28e85b8ca8bfa8dbc6fd6e63873c646dd9b9b23f406d9ff94f9b16aa7de12602df1b1dbbb11299593bf2cc23fc74b4414dfe6d05349155434"}],"licenses":[{"license":{"id":"Apache-2.0"}}],"purl":"pkg:npm/typescript@7.0.2","externalReferences":[{"url":"https://github.com/microsoft/TypeScript/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs.url\""},{"url":"https://github.com/microsoft/TypeScript.git","type":"vcs","comment":"as detected from PackageJson property \"repository.url\""},{"url":"https://www.typescriptlang.org/","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=16.20.0"}]},{"type":"library","name":"vitest","version":"5.0.1","bom-ref":"pkg:npm/vitest@5.0.1","author":"Anthony Fu <anthonyfu117@hotmail.com>","description":"Next generation testing framework powered by Vite","hashes":[{"alg":"SHA-512","content":"880f799506ca124bebb5391d0209d66d77db8a95a28967bf8439763f9b4c8ba585c03efa1b43715c01a9fccf4439c62ea5e18946beb0a6931cec2a00e354d08e"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/vitest@5.0.1","externalReferences":[{"url":"https://github.com/vitest-dev/vitest/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs.url\""},{"url":"git+https://github.com/vitest-dev/vitest.git#packages/vitest","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://vitest.dev","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":"^22.12.0 || ^24.0.0 || >=26.0.0"}]}]},"component":{"type":"library","name":"component-surfaces","group":"@stealthscale","version":"0.0.1","bom-ref":"pkg:npm/%40stealthscale/component-surfaces@0.0.1","description":"Draws the surface under a thing, and the rule between two of them.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/component-surfaces@0.0.1","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#components/surfaces","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/components/surfaces#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}],"evidence":{"licenses":[{"license":{"name":"file: LICENSE","text":{"content":"TUlUIExpY2Vuc2UKCkNvcHlyaWdodCAoYykgMjAyNiBTdGVhbHRoIFNjYWxlIEIuVi4KClBlcm1pc3Npb24gaXMgaGVyZWJ5IGdyYW50ZWQsIGZyZWUgb2YgY2hhcmdlLCB0byBhbnkgcGVyc29uIG9idGFpbmluZyBhIGNvcHkKb2YgdGhpcyBzb2Z0d2FyZSBhbmQgYXNzb2NpYXRlZCBkb2N1bWVudGF0aW9uIGZpbGVzICh0aGUgIlNvZnR3YXJlIiksIHRvIGRlYWwKaW4gdGhlIFNvZnR3YXJlIHdpdGhvdXQgcmVzdHJpY3Rpb24sIGluY2x1ZGluZyB3aXRob3V0IGxpbWl0YXRpb24gdGhlIHJpZ2h0cwp0byB1c2UsIGNvcHksIG1vZGlmeSwgbWVyZ2UsIHB1Ymxpc2gsIGRpc3RyaWJ1dGUsIHN1YmxpY2Vuc2UsIGFuZC9vciBzZWxsCmNvcGllcyBvZiB0aGUgU29mdHdhcmUsIGFuZCB0byBwZXJtaXQgcGVyc29ucyB0byB3aG9tIHRoZSBTb2Z0d2FyZSBpcwpmdXJuaXNoZWQgdG8gZG8gc28sIHN1YmplY3QgdG8gdGhlIGZvbGxvd2luZyBjb25kaXRpb25zOgoKVGhlIGFib3ZlIGNvcHlyaWdodCBub3RpY2UgYW5kIHRoaXMgcGVybWlzc2lvbiBub3RpY2Ugc2hhbGwgYmUgaW5jbHVkZWQgaW4gYWxsCmNvcGllcyBvciBzdWJzdGFudGlhbCBwb3J0aW9ucyBvZiB0aGUgU29mdHdhcmUuCgpUSEUgU09GVFdBUkUgSVMgUFJPVklERUQgIkFTIElTIiwgV0lUSE9VVCBXQVJSQU5UWSBPRiBBTlkgS0lORCwgRVhQUkVTUyBPUgpJTVBMSUVELCBJTkNMVURJTkcgQlVUIE5PVCBMSU1JVEVEIFRPIFRIRSBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSwKRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVC4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFCkFVVEhPUlMgT1IgQ09QWVJJR0hUIEhPTERFUlMgQkUgTElBQkxFIEZPUiBBTlkgQ0xBSU0sIERBTUFHRVMgT1IgT1RIRVIKTElBQklMSVRZLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgVE9SVCBPUiBPVEhFUldJU0UsIEFSSVNJTkcgRlJPTSwKT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgU09GVFdBUkUgT1IgVEhFIFVTRSBPUiBPVEhFUiBERUFMSU5HUyBJTiBUSEUKU09GVFdBUkUuCg==","contentType":"text/plain","encoding":"base64"}}}]}},"supplier":{"name":"Stealth Scale B.V.","url":["https://stealthscale.io"]}},"components":[],"dependencies":[{"ref":"pkg:npm/%40stealthscale/component-surfaces@0.0.1"}]}
@@ -0,0 +1,145 @@
1
+ import "@stealthscale/theme/authoring";
2
+ import { ComponentProps, ReactElement } from "react";
3
+ //#region src/card/aside.d.ts
4
+ /**
5
+ * Renders the aside slot at the end of the header.
6
+ */
7
+ declare const Aside: import("@stealthscale/theme").StyledComponent<"div", {}>;
8
+ /**
9
+ * Describes the props of `Aside`: the props of a `div`.
10
+ */
11
+ type AsideProps = ComponentProps<typeof Aside>;
12
+ //#endregion
13
+ //#region src/card/content.d.ts
14
+ /**
15
+ * Renders the content slot.
16
+ */
17
+ declare const Content: import("@stealthscale/theme").StyledComponent<"div", {}>;
18
+ /**
19
+ * Describes the props of `Content`: the props of a `div`.
20
+ */
21
+ type ContentProps = ComponentProps<typeof Content>;
22
+ //#endregion
23
+ //#region src/card/description.d.ts
24
+ /**
25
+ * Renders the description slot under the title.
26
+ */
27
+ declare const Description: import("@stealthscale/theme").StyledComponent<"p", {}>;
28
+ /**
29
+ * Describes the props of `Description`: the props of a `p`.
30
+ */
31
+ type DescriptionProps = ComponentProps<typeof Description>;
32
+ //#endregion
33
+ //#region src/card/footer.d.ts
34
+ /**
35
+ * Renders the footer slot.
36
+ */
37
+ declare const Footer: import("@stealthscale/theme").StyledComponent<"div", {}>;
38
+ /**
39
+ * Describes the props of `Footer`: the props of a `div`.
40
+ */
41
+ type FooterProps = ComponentProps<typeof Footer>;
42
+ //#endregion
43
+ //#region src/card/header.d.ts
44
+ /**
45
+ * Renders the header slot.
46
+ */
47
+ declare const Header: import("@stealthscale/theme").StyledComponent<"div", {}>;
48
+ /**
49
+ * Describes the props of `Header`: the props of a `div`.
50
+ */
51
+ type HeaderProps = ComponentProps<typeof Header>;
52
+ //#endregion
53
+ //#region src/card/indicator.d.ts
54
+ /**
55
+ * Renders the indicator slot at the start of the header.
56
+ */
57
+ declare const Indicator: import("@stealthscale/theme").StyledComponent<"div", {}>;
58
+ /**
59
+ * Describes the props of `Indicator`: the props of a `div`.
60
+ */
61
+ type IndicatorProps = ComponentProps<typeof Indicator>;
62
+ //#endregion
63
+ //#region src/card/media.d.ts
64
+ /**
65
+ * Renders the media slot.
66
+ */
67
+ declare const Media: import("@stealthscale/theme").StyledComponent<"div", {}>;
68
+ /**
69
+ * Describes the props of `Media`: the props of a `div`.
70
+ */
71
+ type MediaProps = ComponentProps<typeof Media>;
72
+ //#endregion
73
+ //#region src/card/overlay.d.ts
74
+ /**
75
+ * Renders the overlay slot.
76
+ */
77
+ declare const Overlay: import("@stealthscale/theme").StyledComponent<"div", {}>;
78
+ /**
79
+ * Describes the props of `Overlay`: the props of a `div`.
80
+ */
81
+ type OverlayProps = ComponentProps<typeof Overlay>;
82
+ //#endregion
83
+ //#region src/card/metrics.d.ts
84
+ /**
85
+ * Selects one of the four sizes a card offers.
86
+ */
87
+ type Step = "lg" | "md" | "sm" | "xl";
88
+ //#endregion
89
+ //#region src/card/root.d.ts
90
+ /**
91
+ * Renders the root slot and provides the recipe's variants to the other parts.
92
+ */
93
+ declare const Styled: import("@stealthscale/theme").StyledComponent<"article", {
94
+ readonly disabled?: boolean;
95
+ readonly divided?: boolean;
96
+ readonly effect?: "glow";
97
+ readonly interactive?: boolean;
98
+ readonly justify?: import("@stealthscale/theme/authoring").Justify;
99
+ readonly motion?: "fade" | "reveal" | "rise";
100
+ readonly orientation?: "horizontal" | "vertical";
101
+ readonly palette?: "accent" | "error" | "info" | "neutral" | "primary" | "secondary" | "success" | "warning";
102
+ readonly radius?: import("@stealthscale/theme/authoring").Corner;
103
+ readonly scrim?: boolean;
104
+ readonly size?: Step;
105
+ readonly variant?: "elevated" | "glass" | "outline" | "plain" | "subtle";
106
+ }>;
107
+ /**
108
+ * Describes the props of `Root`: the recipe's variants and the props of an `article`.
109
+ */
110
+ type RootProps = ComponentProps<typeof Styled>;
111
+ /**
112
+ * Renders the root, with `aria-disabled` on a disabled card.
113
+ *
114
+ * @remarks
115
+ * `aria-disabled` announces the card's content as unavailable and marks it inactive for contrast
116
+ * checks. An `aria-disabled` the caller passes takes precedence.
117
+ * @param props - The recipe's variants and the props of an `article`.
118
+ * @returns The root element.
119
+ */
120
+ declare function Root(props: RootProps): ReactElement;
121
+ //#endregion
122
+ //#region src/card/section.d.ts
123
+ /**
124
+ * Renders the section slot.
125
+ */
126
+ declare const Section: import("@stealthscale/theme").StyledComponent<"div", {}>;
127
+ /**
128
+ * Describes the props of `Section`: the props of a `div`.
129
+ */
130
+ type SectionProps = ComponentProps<typeof Section>;
131
+ //#endregion
132
+ //#region src/card/title.d.ts
133
+ /**
134
+ * Renders the title slot in the header grid's second column.
135
+ */
136
+ declare const Title: import("@stealthscale/theme").StyledComponent<"h3", {}>;
137
+ /**
138
+ * Describes the props of `Title`: the props of an `h3`.
139
+ */
140
+ type TitleProps = ComponentProps<typeof Title>;
141
+ declare namespace index_d_exports {
142
+ export { Aside, AsideProps, Content, ContentProps, Description, DescriptionProps, Footer, FooterProps, Header, HeaderProps, Indicator, IndicatorProps, Media, MediaProps, Overlay, OverlayProps, Root, RootProps, Section, SectionProps, Title, TitleProps };
143
+ }
144
+ //#endregion
145
+ export { index_d_exports as Card };
package/dist/index.js ADDED
@@ -0,0 +1,124 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { t as recipe } from "./recipe-BSMN8WoK.js";
3
+ import { createSlotRecipeContext } from "@stealthscale/theme";
4
+ import { createElement } from "react";
5
+ //#region src/card/context.ts
6
+ /**
7
+ * Binds the card slot recipe to the React context its parts share.
8
+ *
9
+ * @remarks
10
+ * The binding is a separate module from `recipe.ts`, because an application's style compiler
11
+ * imports the recipe at build time without the React runtime.
12
+ */
13
+ /**
14
+ * Factories that bind the root, which resolves the variants, and every other part, which reads
15
+ * them.
16
+ */
17
+ const { withContext, withProvider } = createSlotRecipeContext(recipe);
18
+ //#endregion
19
+ //#region src/card/aside.ts
20
+ /**
21
+ * Renders the aside slot at the end of the header.
22
+ */
23
+ const Aside = withContext("div", "aside");
24
+ //#endregion
25
+ //#region src/card/content.ts
26
+ /**
27
+ * Renders the content slot.
28
+ */
29
+ const Content = withContext("div", "content");
30
+ //#endregion
31
+ //#region src/card/description.ts
32
+ /**
33
+ * Renders the description slot under the title.
34
+ */
35
+ const Description = withContext("p", "description");
36
+ //#endregion
37
+ //#region src/card/footer.ts
38
+ /**
39
+ * Renders the footer slot.
40
+ */
41
+ const Footer = withContext("div", "footer");
42
+ //#endregion
43
+ //#region src/card/header.ts
44
+ /**
45
+ * Renders the header slot.
46
+ */
47
+ const Header = withContext("div", "header");
48
+ //#endregion
49
+ //#region src/card/indicator.ts
50
+ /**
51
+ * Renders the indicator slot at the start of the header.
52
+ */
53
+ const Indicator = withContext("div", "indicator");
54
+ //#endregion
55
+ //#region src/card/media.ts
56
+ /**
57
+ * Renders the media slot.
58
+ */
59
+ const Media = withContext("div", "media");
60
+ //#endregion
61
+ //#region src/card/overlay.ts
62
+ /**
63
+ * Renders the overlay slot.
64
+ */
65
+ const Overlay = withContext("div", "overlay");
66
+ //#endregion
67
+ //#region src/card/root.ts
68
+ /**
69
+ * Renders the panel that contains every band of a card.
70
+ *
71
+ * @remarks
72
+ * The element is an `article`, which a screen reader lists and moves between. Name it: point
73
+ * `aria-labelledby` at the title's `id`, or pass `aria-label`. Pass `as="div"` for a card that is
74
+ * part of its surroundings. The root resolves every variant, and the other parts read them from
75
+ * context.
76
+ */
77
+ /**
78
+ * Renders the root slot and provides the recipe's variants to the other parts.
79
+ */
80
+ const Styled = withProvider("article", "root");
81
+ /**
82
+ * Renders the root, with `aria-disabled` on a disabled card.
83
+ *
84
+ * @remarks
85
+ * `aria-disabled` announces the card's content as unavailable and marks it inactive for contrast
86
+ * checks. An `aria-disabled` the caller passes takes precedence.
87
+ * @param props - The recipe's variants and the props of an `article`.
88
+ * @returns The root element.
89
+ */
90
+ function Root(props) {
91
+ return createElement(Styled, props.disabled === true ? {
92
+ "aria-disabled": true,
93
+ ...props
94
+ } : props);
95
+ }
96
+ //#endregion
97
+ //#region src/card/section.ts
98
+ /**
99
+ * Renders the section slot.
100
+ */
101
+ const Section = withContext("div", "section");
102
+ //#endregion
103
+ //#region src/card/title.ts
104
+ /**
105
+ * Renders the title slot in the header grid's second column.
106
+ */
107
+ const Title = withContext("h3", "title");
108
+ //#endregion
109
+ //#region src/card/index.ts
110
+ var card_exports = /* @__PURE__ */ __exportAll({
111
+ Aside: () => Aside,
112
+ Content: () => Content,
113
+ Description: () => Description,
114
+ Footer: () => Footer,
115
+ Header: () => Header,
116
+ Indicator: () => Indicator,
117
+ Media: () => Media,
118
+ Overlay: () => Overlay,
119
+ Root: () => Root,
120
+ Section: () => Section,
121
+ Title: () => Title
122
+ });
123
+ //#endregion
124
+ export { card_exports as Card };
@@ -0,0 +1,511 @@
1
+ import { PALETTES, below, cornerVariants, defineSlotRecipe, dense, justifyVariants, motionVariants, onSlot, onSlots, paletteVariants, sizeVariants, surface } from "@stealthscale/theme/authoring";
2
+ //#region src/card/metrics.ts
3
+ /**
4
+ * Computes the geometry the card's size axis and bleeding bands are written from.
5
+ *
6
+ * @remarks
7
+ * The root publishes its inset and its gap as custom properties at every size. `Media` and
8
+ * `Section` read them back, so a band bleeds to the card's edge and spaces its rules at any size
9
+ * and density without one compound per size. The module is separate from the recipe so the recipe
10
+ * is under the 300-line limit.
11
+ */
12
+ /**
13
+ * Lists the sizes the card is styled at, smallest first.
14
+ */
15
+ const STEPS = [
16
+ "sm",
17
+ "md",
18
+ "lg",
19
+ "xl"
20
+ ];
21
+ /**
22
+ * Class name of the recipe, which selectors across parts are built from.
23
+ */
24
+ const CLASS = "card";
25
+ /**
26
+ * Custom property on the root that contains the root's inset.
27
+ */
28
+ const INSET = "--card-inset";
29
+ /**
30
+ * Custom property on the root that contains the gap between two bands.
31
+ */
32
+ const GAP = "--card-gap";
33
+ /**
34
+ * Negative margin that cancels the root's inset.
35
+ */
36
+ const BLEED = `calc(-1 * var(${INSET}))`;
37
+ /**
38
+ * Border colour and style of a rule between two bands.
39
+ */
40
+ const RULE = {
41
+ borderColor: "border",
42
+ borderStyle: "solid"
43
+ };
44
+ /**
45
+ * Maps each size to the size token of an image in the indicator: 32, 40, 48 and 56px.
46
+ */
47
+ const AVATAR = {
48
+ lg: "12",
49
+ md: "10",
50
+ sm: "8",
51
+ xl: "14"
52
+ };
53
+ /**
54
+ * Maps each size to the gap token between the header's text and the indicator or the aside: 8,
55
+ * 12, 16 and 16px.
56
+ */
57
+ const FLANK = {
58
+ lg: "xl",
59
+ md: "lg",
60
+ sm: "md",
61
+ xl: "xl"
62
+ };
63
+ /**
64
+ * Returns the root's inset and gap at a size, published as custom properties and applied as the
65
+ * root's padding and gap.
66
+ *
67
+ * @remarks
68
+ * The properties contain the density-scaled lengths, so a band that bleeds by the inset ends at
69
+ * the edge under any density.
70
+ * @param size - The card's size.
71
+ * @returns The root's size styles.
72
+ */
73
+ function spaced(size) {
74
+ return {
75
+ [GAP]: dense(`{spacing.gap.${size}}`),
76
+ gap: `var(${GAP})`,
77
+ [INSET]: dense(`{spacing.inset.${size}}`),
78
+ padding: `var(${INSET})`
79
+ };
80
+ }
81
+ /**
82
+ * Returns the title's text style: the heading one size smaller than the card, or `label.lg` at
83
+ * `sm`.
84
+ *
85
+ * @remarks
86
+ * One size smaller than `heading.sm` is body text, so the smallest card takes the largest label
87
+ * instead.
88
+ * @param size - The card's size.
89
+ * @returns The title's styles.
90
+ */
91
+ function titled(size) {
92
+ return { textStyle: size === "sm" ? "label.lg" : `heading.${below(size)}` };
93
+ }
94
+ /**
95
+ * Returns the indicator's styles at a size: the gap to the title, and the size of an image or an
96
+ * icon inside it.
97
+ *
98
+ * @remarks
99
+ * An image anywhere in the indicator is a round avatar of 32 to 56px, so a skeleton around it
100
+ * keeps its size. An icon placed directly in the indicator takes the icon size of the card's
101
+ * size. The gap is a margin on the indicator rather than a column gap on the header, so a header
102
+ * without an indicator starts its title at the inset.
103
+ * @param size - The card's size.
104
+ * @returns The indicator's styles.
105
+ */
106
+ function marked(size) {
107
+ return {
108
+ "& > svg": { boxSize: dense(`{sizes.icon.${size}}`) },
109
+ "& img": {
110
+ borderRadius: "full",
111
+ boxSize: dense(`{sizes.${AVATAR[size]}}`),
112
+ objectFit: "cover"
113
+ },
114
+ marginInlineEnd: dense(`{spacing.gap.${FLANK[size]}}`)
115
+ };
116
+ }
117
+ /**
118
+ * Returns the aside's start margin at a size, the gap between the header's text and the aside.
119
+ *
120
+ * @param size - The card's size.
121
+ * @returns The aside's styles.
122
+ */
123
+ function asided(size) {
124
+ return { marginInlineStart: dense(`{spacing.gap.${FLANK[size]}}`) };
125
+ }
126
+ /**
127
+ * Returns the styles that bleed a band of a vertical card to the card's edges.
128
+ *
129
+ * @remarks
130
+ * The band always extends to both inline edges. It extends to the top edge only as the root's
131
+ * first child and to the bottom edge only as its last child, so a band between two others keeps
132
+ * the gap above and below it.
133
+ * @returns The band's styles.
134
+ */
135
+ function bled() {
136
+ return {
137
+ "&:first-child": { marginBlockStart: BLEED },
138
+ "&:last-child": { marginBlockEnd: BLEED },
139
+ marginInline: BLEED
140
+ };
141
+ }
142
+ /**
143
+ * Returns the styles of a band that renders the rule above it: a hairline across the card's full
144
+ * width with the inset on both sides.
145
+ *
146
+ * @remarks
147
+ * The band extends to both side edges and pads its content back to the inset. The space above
148
+ * the rule is the root's gap plus a margin of the inset less the gap, and the space below it is
149
+ * the inset as padding. Every rule in a card is rendered by the lower band of its boundary, so no
150
+ * boundary has two rules.
151
+ * @returns The band's styles.
152
+ */
153
+ function ruled() {
154
+ const inset = `var(${INSET})`;
155
+ return {
156
+ ...RULE,
157
+ borderBlockStartWidth: "hairline",
158
+ marginBlockStart: `calc(${inset} - var(${GAP}))`,
159
+ marginInline: BLEED,
160
+ paddingBlockStart: inset,
161
+ paddingInline: inset
162
+ };
163
+ }
164
+ /**
165
+ * Returns the styles of a section, a band that bleeds to the card's side edges with its content at
166
+ * the inset.
167
+ *
168
+ * @remarks
169
+ * A section renders the rule above it unless it is the first band or follows the picture. As the
170
+ * first band it extends to the top edge, and as the last band to the bottom edge, with the inset
171
+ * as padding. The band after a section renders the rule below it, through {@link ruled}.
172
+ * @returns The section's styles.
173
+ */
174
+ function sectioned() {
175
+ const inset = `var(${INSET})`;
176
+ return {
177
+ "&:first-child": {
178
+ marginBlockStart: BLEED,
179
+ paddingBlockStart: inset
180
+ },
181
+ "&:last-child": {
182
+ marginBlockEnd: BLEED,
183
+ paddingBlockEnd: inset
184
+ },
185
+ [`&:not(:first-child, .${CLASS}__media + *)`]: ruled(),
186
+ marginInline: BLEED,
187
+ paddingInline: inset
188
+ };
189
+ }
190
+ //#endregion
191
+ //#region src/card/recipe.ts
192
+ /**
193
+ * Declares the card slot recipe: a panel of bands, with a picture, a header, content, sections and
194
+ * a footer.
195
+ *
196
+ * @remarks
197
+ * The root is a flex column in both orientations. In the horizontal orientation the picture is
198
+ * positioned along the leading third and the root pads its start past it, so every other band
199
+ * still stacks in one column. The header is a three-column grid: the indicator and the aside each
200
+ * span the title and the description rows and are centred on them. The root clips its content, so
201
+ * a bled picture takes the root's corners.
202
+ */
203
+ /**
204
+ * Width of the picture along the leading side of a horizontal card.
205
+ */
206
+ const SIDE = "33%";
207
+ /**
208
+ * Selects the link in the title that an interactive card stretches over its whole face.
209
+ */
210
+ const TITLE_LINK = `.${CLASS}__title > a`;
211
+ /**
212
+ * Styles a card as an elevated panel at the md size, in the palette of its surroundings.
213
+ */
214
+ const recipe = defineSlotRecipe({
215
+ base: {
216
+ aside: {
217
+ alignItems: "center",
218
+ display: "flex",
219
+ flex: "0 0 auto",
220
+ gridColumn: "3",
221
+ gridRow: "1 / span 2"
222
+ },
223
+ content: {
224
+ display: "flex",
225
+ flex: "1",
226
+ flexDirection: "column",
227
+ gap: dense("{spacing.gap.sm}")
228
+ },
229
+ description: {
230
+ color: "fg.muted",
231
+ gridColumn: "2",
232
+ textStyle: "body.sm"
233
+ },
234
+ footer: {
235
+ alignItems: "center",
236
+ display: "flex",
237
+ flexWrap: "wrap",
238
+ gap: dense("{spacing.gap.sm}")
239
+ },
240
+ header: {
241
+ alignItems: "center",
242
+ display: "grid",
243
+ gridTemplateColumns: "auto 1fr auto"
244
+ },
245
+ indicator: {
246
+ alignItems: "center",
247
+ display: "flex",
248
+ flex: "0 0 auto",
249
+ gridColumn: "1",
250
+ gridRow: "span 2"
251
+ },
252
+ media: {
253
+ "& > img": {
254
+ display: "block",
255
+ inlineSize: "full"
256
+ },
257
+ overflow: "hidden",
258
+ position: "relative"
259
+ },
260
+ /**
261
+ * The overlay covers the picture and places its content at the bottom-start corner.
262
+ *
263
+ * @remarks
264
+ * The overlay passes the pointer through, and its children take it back. The `scrim` axis
265
+ * darkens the picture behind text.
266
+ */
267
+ overlay: {
268
+ "& > *": { pointerEvents: "auto" },
269
+ alignItems: "flex-start",
270
+ display: "flex",
271
+ flexDirection: "column",
272
+ gap: dense("{spacing.gap.sm}"),
273
+ inset: "0",
274
+ justifyContent: "flex-end",
275
+ padding: `var(${INSET})`,
276
+ pointerEvents: "none",
277
+ position: "absolute"
278
+ },
279
+ root: {
280
+ ...surface(),
281
+ [`& > .${CLASS}__section + :is(.${CLASS}__content, .${CLASS}__footer)`]: ruled(),
282
+ display: "flex",
283
+ flexDirection: "column",
284
+ overflow: "hidden",
285
+ position: "relative"
286
+ },
287
+ section: sectioned(),
288
+ title: {
289
+ fontWeight: "semibold",
290
+ gridColumn: "2"
291
+ }
292
+ },
293
+ className: CLASS,
294
+ compoundVariants: [
295
+ (
296
+ /**
297
+ * Sets the border of an outline card in a palette to the palette's muted role, the edge every
298
+ * outlined look draws.
299
+ */
300
+ {
301
+ css: { root: { borderColor: "colorPalette.muted" } },
302
+ name: "toned",
303
+ palette: [...PALETTES],
304
+ variant: "outline"
305
+ }),
306
+ (
307
+ /**
308
+ * Fills a subtle card in a palette with the palette's subtle role and sets its border to the
309
+ * palette's muted role.
310
+ */
311
+ {
312
+ css: { root: {
313
+ background: "colorPalette.subtle",
314
+ borderColor: "colorPalette.muted"
315
+ } },
316
+ name: "tinted",
317
+ palette: [...PALETTES],
318
+ variant: "subtle"
319
+ }),
320
+ (
321
+ /**
322
+ * An interactive elevated card raises its shadow one step under the pointer.
323
+ */
324
+ {
325
+ css: { root: { _hover: { boxShadow: "lg" } } },
326
+ interactive: true,
327
+ name: "lifted",
328
+ variant: "elevated"
329
+ })
330
+ ],
331
+ defaultVariants: {
332
+ justify: "end",
333
+ orientation: "vertical",
334
+ radius: "l2",
335
+ size: "md",
336
+ variant: "elevated"
337
+ },
338
+ jsx: [/^Card(\.\w+)?$/u],
339
+ slots: [
340
+ "root",
341
+ "media",
342
+ "overlay",
343
+ "header",
344
+ "indicator",
345
+ "title",
346
+ "description",
347
+ "aside",
348
+ "content",
349
+ "section",
350
+ "footer"
351
+ ],
352
+ staticCss: [{ palette: [...PALETTES] }],
353
+ variants: {
354
+ /**
355
+ * Unavailable look: the card at the disabled opacity, with no pointer events on any band.
356
+ *
357
+ * @remarks
358
+ * The styles do not remove the title's link from the tab order. Render the title without a
359
+ * link, or give the link `aria-disabled` and no `href`.
360
+ */
361
+ disabled: { true: { root: {
362
+ opacity: "disabled",
363
+ pointerEvents: "none"
364
+ } } },
365
+ /**
366
+ * Rules under the header and above the footer.
367
+ *
368
+ * @remarks
369
+ * The band after the header and the footer each render the rule above them through `ruled`,
370
+ * so both rules span the card's full width with the inset on both sides, like a section's.
371
+ */
372
+ divided: { true: {
373
+ footer: ruled(),
374
+ root: { [`& > .${CLASS}__header + :is(.${CLASS}__content, .${CLASS}__section, .${CLASS}__footer)`]: ruled() }
375
+ } },
376
+ /**
377
+ * Halo around the card in the palette's solid at half opacity.
378
+ *
379
+ * @remarks
380
+ * The axis offers `glow` and no `pulse`. A pulse animates `box-shadow` on the root, whose
381
+ * `animation` the `motion` axis already sets, and the root clips a pseudo-element's shadow.
382
+ */
383
+ effect: onSlot("root", { glow: { layerStyle: "glow.lg" } }),
384
+ /**
385
+ * Whole-card press through the link in the title.
386
+ *
387
+ * @remarks
388
+ * The title's link stretches a pseudo-element over the root, so a click anywhere follows the
389
+ * link and the link keeps the focus and the accessible name. Every other link and button in
390
+ * the card is positioned, so it paints over the pseudo-element and takes its own clicks. The
391
+ * root renders the focus ring while the title's link is focused, in the palette's
392
+ * `focusRing`.
393
+ */
394
+ interactive: { true: {
395
+ root: {
396
+ _hover: { borderColor: "border.emphasized" },
397
+ [`&:has(${TITLE_LINK}:focus-visible)`]: {
398
+ outlineColor: "colorPalette.focusRing",
399
+ outlineOffset: "ring",
400
+ outlineStyle: "solid",
401
+ outlineWidth: "ring"
402
+ },
403
+ [`& :is(a[href], button):not(${TITLE_LINK})`]: { position: "relative" },
404
+ cursor: "button",
405
+ transitionDuration: "press",
406
+ transitionProperty: "common",
407
+ transitionTimingFunction: "press"
408
+ },
409
+ title: { "& > a::after": {
410
+ content: "\"\"",
411
+ inset: "0",
412
+ position: "absolute"
413
+ } }
414
+ } },
415
+ /**
416
+ * Alignment of the footer's controls along the row.
417
+ */
418
+ justify: onSlot("footer", justifyVariants()),
419
+ motion: onSlot("root", motionVariants([
420
+ "fade",
421
+ "rise",
422
+ "reveal"
423
+ ])),
424
+ /**
425
+ * Placement of the picture: bled across the top of a vertical card, or along the leading third
426
+ * of a horizontal one.
427
+ *
428
+ * @remarks
429
+ * A horizontal card pads its start by the picture's width plus the inset only when it
430
+ * contains a picture, and the picture fills its full height with `object-fit: cover`.
431
+ */
432
+ orientation: {
433
+ horizontal: {
434
+ media: {
435
+ "& > img": {
436
+ blockSize: "full",
437
+ objectFit: "cover"
438
+ },
439
+ inlineSize: SIDE,
440
+ insetBlock: "0",
441
+ insetInlineStart: "0",
442
+ position: "absolute"
443
+ },
444
+ root: { [`&:has(> .${CLASS}__media)`]: { paddingInlineStart: `calc(${SIDE} + var(${INSET}))` } }
445
+ },
446
+ vertical: { media: bled() }
447
+ },
448
+ /**
449
+ * Palette of the card's tint, border, focus ring and glow.
450
+ *
451
+ * @remarks
452
+ * The outline look sets its border to the palette's border role, and the subtle look also
453
+ * fills with the palette's subtle role. Without a palette the card inherits the palette of
454
+ * its surroundings.
455
+ */
456
+ palette: onSlot("root", paletteVariants()),
457
+ radius: onSlot("root", cornerVariants()),
458
+ /**
459
+ * Scrim behind the overlay: 64% black across the lower half of the picture, fading to
460
+ * transparent at the top.
461
+ *
462
+ * @remarks
463
+ * The scrim declares the dark color scheme on the overlay, so `fg` and every semantic color
464
+ * in it take their dark-mode values in both modes. Set it for text over a picture. A badge
465
+ * has its own fill and does not need it.
466
+ */
467
+ scrim: { true: { overlay: {
468
+ backgroundImage: "linear-gradient(to top, {colors.blackAlpha.700}, {colors.blackAlpha.700} 50%, transparent)",
469
+ color: "fg",
470
+ colorScheme: "dark"
471
+ } } },
472
+ /**
473
+ * Inset, gap, title style, header gaps and indicator media at each size.
474
+ */
475
+ size: onSlots({
476
+ aside: sizeVariants(asided, STEPS),
477
+ indicator: sizeVariants(marked, STEPS),
478
+ root: sizeVariants(spaced, STEPS),
479
+ title: sizeVariants(titled, STEPS)
480
+ }),
481
+ /**
482
+ * Treatment of the panel.
483
+ *
484
+ * @remarks
485
+ * `plain` removes the fill, the border and the shadow, and keeps the bands and the inset.
486
+ */
487
+ variant: {
488
+ elevated: { root: {
489
+ borderColor: "transparent",
490
+ boxShadow: "md"
491
+ } },
492
+ glass: { root: {
493
+ boxShadow: "none",
494
+ layerStyle: "glass"
495
+ } },
496
+ outline: { root: { boxShadow: "none" } },
497
+ plain: { root: {
498
+ background: "transparent",
499
+ borderColor: "transparent",
500
+ boxShadow: "none"
501
+ } },
502
+ subtle: { root: {
503
+ background: "bg.subtle",
504
+ borderColor: "transparent",
505
+ boxShadow: "none"
506
+ } }
507
+ }
508
+ }
509
+ });
510
+ //#endregion
511
+ export { recipe as t };
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };
@@ -0,0 +1,11 @@
1
+ //#region src/theme.d.ts
2
+ /**
3
+ * Publishes the preset an application's style compiler installs to pick up this package's recipes.
4
+ *
5
+ * @remarks
6
+ * The list of recipes is maintained by hand. The package's own specification fails when a recipe
7
+ * file is missing from it, which is why nothing generates the list.
8
+ */
9
+ declare const _default: import("@stealthscale/theme/authoring").Preset;
10
+ //#endregion
11
+ export { _default as default };
package/dist/theme.js ADDED
@@ -0,0 +1,16 @@
1
+ import { t as recipe } from "./recipe-BSMN8WoK.js";
2
+ import { definePreset } from "@stealthscale/theme/authoring";
3
+ //#region src/theme.ts
4
+ /**
5
+ * Publishes the preset an application's style compiler installs to pick up this package's recipes.
6
+ *
7
+ * @remarks
8
+ * The list of recipes is maintained by hand. The package's own specification fails when a recipe
9
+ * file is missing from it, which is why nothing generates the list.
10
+ */
11
+ var theme_default = definePreset({
12
+ name: "@stealthscale/component-surfaces",
13
+ theme: { extend: { slotRecipes: { card: recipe } } }
14
+ });
15
+ //#endregion
16
+ export { theme_default as default };
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@stealthscale/component-surfaces",
3
+ "version": "0.0.1",
4
+ "description": "Draws the surface under a thing, and the rule between two of them.",
5
+ "keywords": [
6
+ "card",
7
+ "components",
8
+ "react",
9
+ "surfaces"
10
+ ],
11
+ "homepage": "https://github.com/stealth-scale/scale/tree/main/components/surfaces#readme",
12
+ "bugs": "https://github.com/stealth-scale/scale/issues",
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/stealth-scale/scale.git",
17
+ "directory": "components/surfaces"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "LICENSE",
22
+ "README.md"
23
+ ],
24
+ "type": "module",
25
+ "sideEffects": false,
26
+ "imports": {
27
+ "#*": "./src/*"
28
+ },
29
+ "exports": {
30
+ ".": "./dist/index.js",
31
+ "./theme": "./dist/theme.js",
32
+ "./package.json": "./package.json"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "devDependencies": {
38
+ "@arethetypeswrong/core": "^0.18",
39
+ "@stealthscale/component-actions": "^0.1.0",
40
+ "@stealthscale/component-navigation": "^0.1.0",
41
+ "@stealthscale/theme": "^0.3.0",
42
+ "@stealthscale/vite-config": "^0.6.0",
43
+ "@stealthscale/vite-config-i18n": "^0.1.0",
44
+ "@stealthscale/vite-config-react": "^0.8.0",
45
+ "@stealthscale/vite-config-specimen": "^0.1.0",
46
+ "@stealthscale/vite-config-theme": "^0.1.0",
47
+ "@stealthscale/vite-config-typescript": "^0.3.0",
48
+ "@testing-library/react": "^16.3",
49
+ "@types/react": "^19.3",
50
+ "@types/react-dom": "^19.3",
51
+ "happy-dom": "^20.14",
52
+ "lucide-react": "^1.48",
53
+ "oxc-transform-react": "^0.151",
54
+ "publint": "^0.3",
55
+ "react": "^19.3",
56
+ "react-dom": "^19.3",
57
+ "typescript": "^7.0",
58
+ "vitest": "5.0.1"
59
+ },
60
+ "peerDependencies": {
61
+ "@stealthscale/theme": "^0.3.0",
62
+ "react": "^19.3"
63
+ },
64
+ "engines": {
65
+ "node": ">=26.0.0"
66
+ },
67
+ "scripts": {
68
+ "build": "vp pack",
69
+ "check": "vp check",
70
+ "test": "vp test"
71
+ }
72
+ }