@dxlbnl/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +94 -0
  2. package/dist/components/cards/Card.stories.svelte +82 -0
  3. package/dist/components/cards/Card.stories.svelte.d.ts +19 -0
  4. package/dist/components/cards/Card.svelte +28 -0
  5. package/dist/components/cards/Card.svelte.d.ts +12 -0
  6. package/dist/components/cards/NoteCard.stories.svelte +94 -0
  7. package/dist/components/cards/NoteCard.stories.svelte.d.ts +19 -0
  8. package/dist/components/cards/NoteCard.svelte +89 -0
  9. package/dist/components/cards/NoteCard.svelte.d.ts +18 -0
  10. package/dist/components/cards/ProductCard.stories.svelte +98 -0
  11. package/dist/components/cards/ProductCard.stories.svelte.d.ts +19 -0
  12. package/dist/components/cards/ProductCard.svelte +150 -0
  13. package/dist/components/cards/ProductCard.svelte.d.ts +22 -0
  14. package/dist/components/cards/ProjectCard.stories.svelte +88 -0
  15. package/dist/components/cards/ProjectCard.stories.svelte.d.ts +19 -0
  16. package/dist/components/cards/ProjectCard.svelte +109 -0
  17. package/dist/components/cards/ProjectCard.svelte.d.ts +20 -0
  18. package/dist/components/cards/index.d.ts +4 -0
  19. package/dist/components/cards/index.js +4 -0
  20. package/dist/components/data/Accordion.stories.svelte +316 -0
  21. package/dist/components/data/Accordion.stories.svelte.d.ts +19 -0
  22. package/dist/components/data/Accordion.svelte +23 -0
  23. package/dist/components/data/Accordion.svelte.d.ts +9 -0
  24. package/dist/components/data/AccordionItem.svelte +112 -0
  25. package/dist/components/data/AccordionItem.svelte.d.ts +11 -0
  26. package/dist/components/data/Table.composition.stories.svelte +67 -0
  27. package/dist/components/data/Table.composition.stories.svelte.d.ts +19 -0
  28. package/dist/components/data/Table.stories.svelte +137 -0
  29. package/dist/components/data/Table.stories.svelte.d.ts +19 -0
  30. package/dist/components/data/Table.svelte +83 -0
  31. package/dist/components/data/Table.svelte.d.ts +14 -0
  32. package/dist/components/data/Tabs.stories.svelte +386 -0
  33. package/dist/components/data/Tabs.stories.svelte.d.ts +19 -0
  34. package/dist/components/data/Tabs.svelte +142 -0
  35. package/dist/components/data/Tabs.svelte.d.ts +19 -0
  36. package/dist/components/data/index.d.ts +4 -0
  37. package/dist/components/data/index.js +4 -0
  38. package/dist/components/feedback/Modal.stories.svelte +192 -0
  39. package/dist/components/feedback/Modal.stories.svelte.d.ts +4 -0
  40. package/dist/components/feedback/Modal.svelte +185 -0
  41. package/dist/components/feedback/Modal.svelte.d.ts +19 -0
  42. package/dist/components/feedback/Toast.stories.svelte +203 -0
  43. package/dist/components/feedback/Toast.stories.svelte.d.ts +19 -0
  44. package/dist/components/feedback/Toast.svelte +109 -0
  45. package/dist/components/feedback/Toast.svelte.d.ts +15 -0
  46. package/dist/components/feedback/ToastRegion.stories.svelte +193 -0
  47. package/dist/components/feedback/ToastRegion.stories.svelte.d.ts +19 -0
  48. package/dist/components/feedback/ToastRegion.svelte +102 -0
  49. package/dist/components/feedback/ToastRegion.svelte.d.ts +9 -0
  50. package/dist/components/feedback/index.d.ts +3 -0
  51. package/dist/components/feedback/index.js +3 -0
  52. package/dist/components/forms/Checkbox.stories.svelte +103 -0
  53. package/dist/components/forms/Checkbox.stories.svelte.d.ts +19 -0
  54. package/dist/components/forms/Checkbox.svelte +150 -0
  55. package/dist/components/forms/Checkbox.svelte.d.ts +11 -0
  56. package/dist/components/forms/Field.stories.svelte +113 -0
  57. package/dist/components/forms/Field.stories.svelte.d.ts +19 -0
  58. package/dist/components/forms/Field.svelte +77 -0
  59. package/dist/components/forms/Field.svelte.d.ts +17 -0
  60. package/dist/components/forms/Input.stories.svelte +58 -0
  61. package/dist/components/forms/Input.stories.svelte.d.ts +19 -0
  62. package/dist/components/forms/Input.svelte +64 -0
  63. package/dist/components/forms/Input.svelte.d.ts +9 -0
  64. package/dist/components/forms/InputWrap.composition.stories.svelte +32 -0
  65. package/dist/components/forms/InputWrap.composition.stories.svelte.d.ts +19 -0
  66. package/dist/components/forms/InputWrap.stories.svelte +53 -0
  67. package/dist/components/forms/InputWrap.stories.svelte.d.ts +19 -0
  68. package/dist/components/forms/InputWrap.svelte +128 -0
  69. package/dist/components/forms/InputWrap.svelte.d.ts +21 -0
  70. package/dist/components/forms/Radio.stories.svelte +70 -0
  71. package/dist/components/forms/Radio.stories.svelte.d.ts +19 -0
  72. package/dist/components/forms/Radio.svelte +109 -0
  73. package/dist/components/forms/Radio.svelte.d.ts +9 -0
  74. package/dist/components/forms/RadioGroup.stories.svelte +115 -0
  75. package/dist/components/forms/RadioGroup.stories.svelte.d.ts +19 -0
  76. package/dist/components/forms/RadioGroup.svelte +116 -0
  77. package/dist/components/forms/RadioGroup.svelte.d.ts +24 -0
  78. package/dist/components/forms/Select.stories.svelte +168 -0
  79. package/dist/components/forms/Select.stories.svelte.d.ts +19 -0
  80. package/dist/components/forms/Select.svelte +262 -0
  81. package/dist/components/forms/Select.svelte.d.ts +23 -0
  82. package/dist/components/forms/Switch.stories.svelte +86 -0
  83. package/dist/components/forms/Switch.stories.svelte.d.ts +19 -0
  84. package/dist/components/forms/Switch.svelte +113 -0
  85. package/dist/components/forms/Switch.svelte.d.ts +11 -0
  86. package/dist/components/forms/Textarea.stories.svelte +40 -0
  87. package/dist/components/forms/Textarea.stories.svelte.d.ts +19 -0
  88. package/dist/components/forms/Textarea.svelte +66 -0
  89. package/dist/components/forms/Textarea.svelte.d.ts +9 -0
  90. package/dist/components/forms/field-context.d.ts +7 -0
  91. package/dist/components/forms/field-context.js +1 -0
  92. package/dist/components/forms/index.d.ts +9 -0
  93. package/dist/components/forms/index.js +9 -0
  94. package/dist/components/layout/Container.stories.svelte +67 -0
  95. package/dist/components/layout/Container.stories.svelte.d.ts +19 -0
  96. package/dist/components/layout/Container.svelte +52 -0
  97. package/dist/components/layout/Container.svelte.d.ts +14 -0
  98. package/dist/components/layout/Grid.stories.svelte +109 -0
  99. package/dist/components/layout/Grid.stories.svelte.d.ts +19 -0
  100. package/dist/components/layout/Grid.svelte +54 -0
  101. package/dist/components/layout/Grid.svelte.d.ts +19 -0
  102. package/dist/components/layout/Inline.stories.svelte +136 -0
  103. package/dist/components/layout/Inline.stories.svelte.d.ts +19 -0
  104. package/dist/components/layout/Inline.svelte +46 -0
  105. package/dist/components/layout/Inline.svelte.d.ts +19 -0
  106. package/dist/components/layout/Prose.stories.svelte +423 -0
  107. package/dist/components/layout/Prose.stories.svelte.d.ts +19 -0
  108. package/dist/components/layout/Prose.svelte +176 -0
  109. package/dist/components/layout/Prose.svelte.d.ts +12 -0
  110. package/dist/components/layout/Rule.stories.svelte +80 -0
  111. package/dist/components/layout/Rule.stories.svelte.d.ts +19 -0
  112. package/dist/components/layout/Rule.svelte +33 -0
  113. package/dist/components/layout/Rule.svelte.d.ts +9 -0
  114. package/dist/components/layout/Spread.stories.svelte +118 -0
  115. package/dist/components/layout/Spread.stories.svelte.d.ts +19 -0
  116. package/dist/components/layout/Spread.svelte +38 -0
  117. package/dist/components/layout/Spread.svelte.d.ts +16 -0
  118. package/dist/components/layout/Stack.stories.svelte +90 -0
  119. package/dist/components/layout/Stack.stories.svelte.d.ts +19 -0
  120. package/dist/components/layout/Stack.svelte +37 -0
  121. package/dist/components/layout/Stack.svelte.d.ts +16 -0
  122. package/dist/components/layout/index.d.ts +7 -0
  123. package/dist/components/layout/index.js +7 -0
  124. package/dist/components/navigation/Breadcrumb.stories.svelte +122 -0
  125. package/dist/components/navigation/Breadcrumb.stories.svelte.d.ts +19 -0
  126. package/dist/components/navigation/Breadcrumb.svelte +70 -0
  127. package/dist/components/navigation/Breadcrumb.svelte.d.ts +13 -0
  128. package/dist/components/navigation/Nav.stories.svelte +323 -0
  129. package/dist/components/navigation/Nav.stories.svelte.d.ts +19 -0
  130. package/dist/components/navigation/Nav.svelte +257 -0
  131. package/dist/components/navigation/Nav.svelte.d.ts +21 -0
  132. package/dist/components/navigation/index.d.ts +2 -0
  133. package/dist/components/navigation/index.js +2 -0
  134. package/dist/components/patterns/ActivityRow.stories.svelte +45 -0
  135. package/dist/components/patterns/ActivityRow.stories.svelte.d.ts +19 -0
  136. package/dist/components/patterns/ActivityRow.svelte +69 -0
  137. package/dist/components/patterns/ActivityRow.svelte.d.ts +16 -0
  138. package/dist/components/patterns/Alert.stories.svelte +63 -0
  139. package/dist/components/patterns/Alert.stories.svelte.d.ts +19 -0
  140. package/dist/components/patterns/Alert.svelte +91 -0
  141. package/dist/components/patterns/Alert.svelte.d.ts +16 -0
  142. package/dist/components/patterns/CtaBlock.stories.svelte +62 -0
  143. package/dist/components/patterns/CtaBlock.stories.svelte.d.ts +19 -0
  144. package/dist/components/patterns/CtaBlock.svelte +80 -0
  145. package/dist/components/patterns/CtaBlock.svelte.d.ts +16 -0
  146. package/dist/components/patterns/KvList.stories.svelte +48 -0
  147. package/dist/components/patterns/KvList.stories.svelte.d.ts +19 -0
  148. package/dist/components/patterns/KvList.svelte +65 -0
  149. package/dist/components/patterns/KvList.svelte.d.ts +15 -0
  150. package/dist/components/patterns/PageHero.stories.svelte +62 -0
  151. package/dist/components/patterns/PageHero.stories.svelte.d.ts +19 -0
  152. package/dist/components/patterns/PageHero.svelte +62 -0
  153. package/dist/components/patterns/PageHero.svelte.d.ts +14 -0
  154. package/dist/components/patterns/ProgressBar.stories.svelte +83 -0
  155. package/dist/components/patterns/ProgressBar.stories.svelte.d.ts +19 -0
  156. package/dist/components/patterns/ProgressBar.svelte +71 -0
  157. package/dist/components/patterns/ProgressBar.svelte.d.ts +13 -0
  158. package/dist/components/patterns/SectionFoot.stories.svelte +37 -0
  159. package/dist/components/patterns/SectionFoot.stories.svelte.d.ts +19 -0
  160. package/dist/components/patterns/SectionFoot.svelte +70 -0
  161. package/dist/components/patterns/SectionFoot.svelte.d.ts +15 -0
  162. package/dist/components/patterns/SectionHead.stories.svelte +67 -0
  163. package/dist/components/patterns/SectionHead.stories.svelte.d.ts +19 -0
  164. package/dist/components/patterns/SectionHead.svelte +54 -0
  165. package/dist/components/patterns/SectionHead.svelte.d.ts +14 -0
  166. package/dist/components/patterns/StatCard.stories.svelte +59 -0
  167. package/dist/components/patterns/StatCard.stories.svelte.d.ts +19 -0
  168. package/dist/components/patterns/StatCard.svelte +57 -0
  169. package/dist/components/patterns/StatCard.svelte.d.ts +15 -0
  170. package/dist/components/patterns/index.d.ts +9 -0
  171. package/dist/components/patterns/index.js +9 -0
  172. package/dist/components/primitives/Button.stories.svelte +132 -0
  173. package/dist/components/primitives/Button.stories.svelte.d.ts +19 -0
  174. package/dist/components/primitives/Button.svelte +142 -0
  175. package/dist/components/primitives/Button.svelte.d.ts +16 -0
  176. package/dist/components/primitives/Heading.stories.svelte +137 -0
  177. package/dist/components/primitives/Heading.stories.svelte.d.ts +19 -0
  178. package/dist/components/primitives/Heading.svelte +107 -0
  179. package/dist/components/primitives/Heading.svelte.d.ts +23 -0
  180. package/dist/components/primitives/Led.stories.svelte +63 -0
  181. package/dist/components/primitives/Led.stories.svelte.d.ts +19 -0
  182. package/dist/components/primitives/Led.svelte +65 -0
  183. package/dist/components/primitives/Led.svelte.d.ts +11 -0
  184. package/dist/components/primitives/TagPill.stories.svelte +90 -0
  185. package/dist/components/primitives/TagPill.stories.svelte.d.ts +19 -0
  186. package/dist/components/primitives/TagPill.svelte +44 -0
  187. package/dist/components/primitives/TagPill.svelte.d.ts +9 -0
  188. package/dist/components/primitives/Text.stories.svelte +252 -0
  189. package/dist/components/primitives/Text.stories.svelte.d.ts +19 -0
  190. package/dist/components/primitives/Text.svelte +101 -0
  191. package/dist/components/primitives/Text.svelte.d.ts +25 -0
  192. package/dist/components/primitives/index.d.ts +5 -0
  193. package/dist/components/primitives/index.js +5 -0
  194. package/dist/index.d.ts +10 -0
  195. package/dist/index.js +10 -0
  196. package/dist/stores/toast.d.ts +19 -0
  197. package/dist/stores/toast.js +22 -0
  198. package/dist/storybook-utils.d.ts +11 -0
  199. package/dist/storybook-utils.js +29 -0
  200. package/dist/tokens/ColorSwatch.svelte +73 -0
  201. package/dist/tokens/ColorSwatch.svelte.d.ts +10 -0
  202. package/dist/tokens/layout.css +144 -0
  203. package/dist/tokens/patterns.css +281 -0
  204. package/dist/tokens/tokens.css +96 -0
  205. package/dist/tokens/tokens.stories.svelte +107 -0
  206. package/dist/tokens/tokens.stories.svelte.d.ts +18 -0
  207. package/dist/tokens/typography.css +159 -0
  208. package/package.json +62 -0
@@ -0,0 +1,150 @@
1
+ <script lang="ts">
2
+ import type { HTMLAnchorAttributes } from 'svelte/elements'
3
+ import Card from './Card.svelte'
4
+ import Stack from '../layout/Stack.svelte'
5
+ import Spread from '../layout/Spread.svelte'
6
+ import Inline from '../layout/Inline.svelte'
7
+ import Led from '../primitives/Led.svelte'
8
+ import Text from '../primitives/Text.svelte'
9
+ import Heading from '../primitives/Heading.svelte'
10
+
11
+ type StockStatus = 'in-stock' | 'coming-soon' | 'low-stock' | 'out-of-stock'
12
+
13
+ interface Props extends HTMLAnchorAttributes {
14
+ /** HTML element to render as. @default 'a' */
15
+ as?: string
16
+ /** Product SKU code, shown in the card header. */
17
+ sku: string
18
+ /** Product display name. */
19
+ name: string
20
+ /** Short product description. */
21
+ description: string
22
+ /** Formatted price string (e.g. `'€200'`). */
23
+ price?: string
24
+ /** Inventory status — drives the LED colour and CTA label. @default 'out-of-stock' */
25
+ status?: StockStatus
26
+ /** Override the generated CTA label. */
27
+ ctaLabel?: string
28
+ [key: string]: unknown
29
+ }
30
+
31
+ let {
32
+ as = 'a',
33
+ sku,
34
+ name,
35
+ description,
36
+ price,
37
+ status = 'out-of-stock',
38
+ ctaLabel,
39
+ ...rest
40
+ }: Props = $props()
41
+
42
+ const ledColor = $derived(
43
+ status === 'in-stock' ? 'ok' : status === 'out-of-stock' ? 'off' : 'amber'
44
+ )
45
+
46
+ const stockLabel = $derived(
47
+ status === 'in-stock'
48
+ ? 'In Stock'
49
+ : status === 'coming-soon'
50
+ ? 'Coming Soon'
51
+ : status === 'low-stock'
52
+ ? 'Low Stock'
53
+ : 'Out of Stock'
54
+ )
55
+
56
+ const resolvedCtaLabel = $derived(
57
+ ctaLabel ??
58
+ (status === 'in-stock' || status === 'low-stock'
59
+ ? 'BUY NOW'
60
+ : status === 'coming-soon'
61
+ ? 'PRE-ORDER'
62
+ : 'VIEW DETAILS')
63
+ )
64
+ </script>
65
+
66
+ <Card as={as} class="product-card" {...rest}>
67
+ <div class="card-img">
68
+ <Text variant="mono" color="faint">{sku} · PRODUCT</Text>
69
+ </div>
70
+ <div class="card-body">
71
+ <Stack gap="xs">
72
+ <Text variant="eyebrow">{sku}</Text>
73
+ <Heading level={3} size="lg">{name}</Heading>
74
+ <Text variant="mono" case="none" color="dim" class="card-desc">{description}</Text>
75
+ <div class="card-footer-row">
76
+ <Spread>
77
+ {#if price}
78
+ <Inline gap="xs" align="baseline">
79
+ <Text variant="mono" color="amber" size="md">{price}</Text>
80
+ <Text variant="mono" color="faint" size="xs" case="lower">incl. VAT</Text>
81
+ </Inline>
82
+ {/if}
83
+ <Inline gap="xs">
84
+ <Led color={ledColor} aria-hidden="true" />
85
+ <Text variant="mono" size="xs">{stockLabel}</Text>
86
+ </Inline>
87
+ </Spread>
88
+ </div>
89
+ </Stack>
90
+ </div>
91
+ <div class="card-cta">
92
+ <Spread>
93
+ <Text variant="mono">{resolvedCtaLabel}</Text>
94
+ <span aria-hidden="true">→</span>
95
+ </Spread>
96
+ </div>
97
+ </Card>
98
+
99
+ <style>
100
+ .card-body {
101
+ padding: 12px 14px 10px;
102
+ flex: 1;
103
+ display: flex;
104
+ flex-direction: column;
105
+ }
106
+
107
+ .card-footer-row {
108
+ align-items: baseline;
109
+ margin-top: auto;
110
+ padding-top: 8px;
111
+ display: flex;
112
+ justify-content: space-between;
113
+ gap: var(--u2);
114
+ }
115
+
116
+ /* :global needed — .product-card is on Card's root, invisible to Svelte's scoped CSS analysis */
117
+ :global(.product-card) {
118
+ text-decoration: none;
119
+ color: inherit;
120
+ cursor: pointer;
121
+ }
122
+
123
+ .card-img {
124
+ aspect-ratio: 14 / 9;
125
+ background: repeating-linear-gradient(
126
+ 135deg,
127
+ var(--bg-sunken) 0 10px,
128
+ var(--bg-elev) 10px 20px
129
+ );
130
+ border-bottom: 1px solid var(--rule);
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: center;
134
+ }
135
+
136
+ :global(.card-desc) {
137
+ line-height: 1.4;
138
+ }
139
+
140
+ .card-cta {
141
+ border-top: 1px solid var(--rule);
142
+ padding: 10px 14px;
143
+ transition: background var(--transition), color var(--transition);
144
+ }
145
+
146
+ :global(.product-card):hover .card-cta {
147
+ background: var(--amber);
148
+ color: var(--bg);
149
+ }
150
+ </style>
@@ -0,0 +1,22 @@
1
+ import type { HTMLAnchorAttributes } from 'svelte/elements';
2
+ type StockStatus = 'in-stock' | 'coming-soon' | 'low-stock' | 'out-of-stock';
3
+ interface Props extends HTMLAnchorAttributes {
4
+ /** HTML element to render as. @default 'a' */
5
+ as?: string;
6
+ /** Product SKU code, shown in the card header. */
7
+ sku: string;
8
+ /** Product display name. */
9
+ name: string;
10
+ /** Short product description. */
11
+ description: string;
12
+ /** Formatted price string (e.g. `'€200'`). */
13
+ price?: string;
14
+ /** Inventory status — drives the LED colour and CTA label. @default 'out-of-stock' */
15
+ status?: StockStatus;
16
+ /** Override the generated CTA label. */
17
+ ctaLabel?: string;
18
+ [key: string]: unknown;
19
+ }
20
+ declare const ProductCard: import("svelte").Component<Props, {}, "">;
21
+ type ProductCard = ReturnType<typeof ProductCard>;
22
+ export default ProductCard;
@@ -0,0 +1,88 @@
1
+ <script module lang="ts">
2
+ import { defineMeta } from "@storybook/addon-svelte-csf";
3
+ import { expect, within } from "storybook/test";
4
+ import ProjectCard from "./ProjectCard.svelte";
5
+
6
+ const { Story } = defineMeta({
7
+ title: "Cards/ProjectCard",
8
+ component: ProjectCard,
9
+ tags: ["autodocs"],
10
+ });
11
+ </script>
12
+
13
+ <Story name="Open Source" args={{ href: "#zod-fixture", slug: "zod-fixture", title: "zod-fixture", description: "Creating fixtures based on zod schemas automatically.", tags: ["TypeScript", "Open Source"], ctaLabel: "OPEN SOURCE" }}
14
+ play={async ({ canvasElement }) => {
15
+ const canvas = within(canvasElement);
16
+
17
+ // renders as <a> by default
18
+ const root = canvasElement.firstElementChild as HTMLElement;
19
+ await expect(root).toBeVisible();
20
+
21
+ // title renders as h3
22
+ const heading = canvas.getByText("zod-fixture");
23
+ await expect(heading).toBeVisible();
24
+
25
+ // tags render as TagPill spans with tag text
26
+ const tsTag = canvas.getByText("TypeScript");
27
+ await expect(tsTag).toBeVisible();
28
+ const osTag = canvas.getByText("Open Source");
29
+ await expect(osTag).toBeVisible();
30
+
31
+ // footer renders ctaLabel
32
+ const cta = canvas.getByText(/OPEN SOURCE/);
33
+ await expect(cta).toBeVisible();
34
+
35
+ // .card-img hatch gradient present and aspect-ratio is 14/9
36
+ const cardImg = canvasElement.querySelector(".card-img") as HTMLElement;
37
+ const imgBg = getComputedStyle(cardImg).backgroundImage;
38
+ await expect(imgBg).not.toBe("none");
39
+ await expect(getComputedStyle(cardImg).aspectRatio).toBe("14 / 9");
40
+ }} />
41
+
42
+ <Story name="No Tags" args={{ href: "#private-share", slug: "private-share", title: "Private Share", description: "Private file sharing utility." }}
43
+ play={async ({ canvasElement }) => {
44
+ const canvas = within(canvasElement);
45
+
46
+ // Card renders without error
47
+ const root = canvasElement.firstElementChild as HTMLElement;
48
+ await expect(root).toBeVisible();
49
+
50
+ // title heading is visible
51
+ const heading = canvas.getByText("Private Share");
52
+ await expect(heading).toBeVisible();
53
+
54
+ // no .card-tags element rendered (or it has no children) when tags is empty
55
+ const cardTags = canvasElement.querySelector(".card-tags");
56
+ await expect(cardTags).toBeNull();
57
+ }} />
58
+
59
+ <Story name="As Div" args={{ as: "div", slug: "zod-fixture", title: "zod-fixture", description: "Creating fixtures based on zod schemas automatically.", tags: ["TypeScript", "Open Source"], ctaLabel: "OPEN SOURCE" }}
60
+ play={async ({ canvasElement }) => {
61
+ const root = canvasElement.firstElementChild as HTMLElement;
62
+ await expect(root.tagName).toBe("DIV");
63
+ await expect(root).toBeVisible();
64
+ }} />
65
+
66
+ <Story name="Many Tags" args={{ href: "#many-tags", slug: "many-tags", title: "Many Tags Project", description: "A project with many technology tags.", tags: ["TypeScript", "Open Source", "SvelteKit", "Rust", "Embedded", "Hardware"] }}
67
+ play={async ({ canvasElement }) => {
68
+ const canvas = within(canvasElement);
69
+ await expect(canvas.getByText("TypeScript")).toBeVisible();
70
+ await expect(canvas.getByText("Open Source")).toBeVisible();
71
+ await expect(canvas.getByText("SvelteKit")).toBeVisible();
72
+ await expect(canvas.getByText("Rust")).toBeVisible();
73
+ await expect(canvas.getByText("Embedded")).toBeVisible();
74
+ await expect(canvas.getByText("Hardware")).toBeVisible();
75
+ await expect(canvasElement.querySelector(".card-tags")).not.toBeNull();
76
+ await expect(canvasElement.querySelectorAll(".card-tags span").length).toBeGreaterThanOrEqual(6);
77
+ }} />
78
+
79
+ <Story name="No Description" args={{ href: "#no-desc", slug: "no-desc", title: "No Description Project", description: "" }}
80
+ play={async ({ canvasElement }) => {
81
+ const canvas = within(canvasElement);
82
+ const root = canvasElement.firstElementChild;
83
+ await expect(root).toBeVisible();
84
+ await expect(canvas.getByText("No Description Project")).toBeVisible();
85
+ const desc = canvasElement.querySelector(".card-desc");
86
+ await expect(desc).not.toBeNull();
87
+ await expect(desc!.textContent!.trim()).toBe("");
88
+ }} />
@@ -0,0 +1,19 @@
1
+ import ProjectCard from "./ProjectCard.svelte";
2
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
+ $$bindings?: Bindings;
5
+ } & Exports;
6
+ (internal: unknown, props: {
7
+ $$events?: Events;
8
+ $$slots?: Slots;
9
+ }): Exports & {
10
+ $set?: any;
11
+ $on?: any;
12
+ };
13
+ z_$$bindings?: Bindings;
14
+ }
15
+ declare const ProjectCard: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
16
+ [evt: string]: CustomEvent<any>;
17
+ }, {}, {}, string>;
18
+ type ProjectCard = InstanceType<typeof ProjectCard>;
19
+ export default ProjectCard;
@@ -0,0 +1,109 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import Card from './Card.svelte'
4
+ import TagPill from '../primitives/TagPill.svelte'
5
+ import Stack from '../layout/Stack.svelte'
6
+ import Spread from '../layout/Spread.svelte'
7
+ import Inline from '../layout/Inline.svelte'
8
+ import Text from '../primitives/Text.svelte'
9
+ import Heading from '../primitives/Heading.svelte'
10
+
11
+ interface Props {
12
+ /** HTML element to render as. @default 'a' */
13
+ as?: string
14
+ /** Project slug, shown in the card header. */
15
+ slug: string
16
+ /** Project display title. */
17
+ title: string
18
+ /** Short project description. */
19
+ description: string
20
+ /** Tag labels rendered as TagPills. @default [] */
21
+ tags?: string[]
22
+ /** CTA label in the card footer. @default 'VIEW PROJECT' */
23
+ ctaLabel?: string
24
+ children?: Snippet
25
+ [key: string]: unknown
26
+ }
27
+
28
+ let {
29
+ as = 'a',
30
+ slug,
31
+ title,
32
+ description,
33
+ tags = [],
34
+ ctaLabel = 'VIEW PROJECT',
35
+ ...rest
36
+ }: Props = $props()
37
+ </script>
38
+
39
+ <Card as={as} class="project-card" {...rest}>
40
+ <div class="card-img">
41
+ <Text variant="eyebrow">{slug.toUpperCase()} · PROJECT</Text>
42
+ </div>
43
+ <div class="card-body">
44
+ <Stack gap="xs">
45
+ {#if tags.length > 0}
46
+ <Inline gap="xs" class="card-tags">
47
+ {#each tags as tag}
48
+ <TagPill>{tag}</TagPill>
49
+ {/each}
50
+ </Inline>
51
+ {/if}
52
+ <Heading level={3} size="lg">{title}</Heading>
53
+ <p class="card-desc">{description}</p>
54
+ </Stack>
55
+ </div>
56
+ <div class="card-cta">
57
+ <Spread>
58
+ <Text variant="mono">{ctaLabel}</Text>
59
+ <span aria-hidden="true">→</span>
60
+ </Spread>
61
+ </div>
62
+ </Card>
63
+
64
+ <style>
65
+ .card-body {
66
+ padding: 12px 14px 10px;
67
+ flex: 1;
68
+ display: flex;
69
+ flex-direction: column;
70
+ }
71
+
72
+ :global(.project-card) {
73
+ text-decoration: none;
74
+ color: inherit;
75
+ cursor: pointer;
76
+ }
77
+
78
+ .card-img {
79
+ aspect-ratio: 14 / 9;
80
+ background: repeating-linear-gradient(
81
+ 135deg,
82
+ var(--bg-sunken) 0 10px,
83
+ var(--bg-elev) 10px 20px
84
+ );
85
+ border-bottom: 1px solid var(--rule);
86
+ display: flex;
87
+ align-items: center;
88
+ justify-content: center;
89
+ }
90
+
91
+ .card-desc {
92
+ font-family: var(--mono);
93
+ font-size: var(--t-mono);
94
+ color: var(--ink-dim);
95
+ line-height: 1.4;
96
+ margin: 0;
97
+ }
98
+
99
+ .card-cta {
100
+ border-top: 1px solid var(--rule);
101
+ padding: 10px 14px;
102
+ transition: background var(--transition), color var(--transition);
103
+ }
104
+
105
+ :global(.project-card):hover .card-cta {
106
+ background: var(--amber);
107
+ color: var(--bg);
108
+ }
109
+ </style>
@@ -0,0 +1,20 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ /** HTML element to render as. @default 'a' */
4
+ as?: string;
5
+ /** Project slug, shown in the card header. */
6
+ slug: string;
7
+ /** Project display title. */
8
+ title: string;
9
+ /** Short project description. */
10
+ description: string;
11
+ /** Tag labels rendered as TagPills. @default [] */
12
+ tags?: string[];
13
+ /** CTA label in the card footer. @default 'VIEW PROJECT' */
14
+ ctaLabel?: string;
15
+ children?: Snippet;
16
+ [key: string]: unknown;
17
+ }
18
+ declare const ProjectCard: import("svelte").Component<Props, {}, "">;
19
+ type ProjectCard = ReturnType<typeof ProjectCard>;
20
+ export default ProjectCard;
@@ -0,0 +1,4 @@
1
+ export { default as Card } from './Card.svelte';
2
+ export { default as ProductCard } from './ProductCard.svelte';
3
+ export { default as ProjectCard } from './ProjectCard.svelte';
4
+ export { default as NoteCard } from './NoteCard.svelte';
@@ -0,0 +1,4 @@
1
+ export { default as Card } from './Card.svelte';
2
+ export { default as ProductCard } from './ProductCard.svelte';
3
+ export { default as ProjectCard } from './ProjectCard.svelte';
4
+ export { default as NoteCard } from './NoteCard.svelte';