@writedocs/generator 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 (95) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +17 -0
  3. package/astro.config.mjs +419 -0
  4. package/bin/writedocs.js +73 -0
  5. package/package.json +79 -0
  6. package/src/assets/wd_watermark.png +0 -0
  7. package/src/assets/wd_watermark_dark.png +0 -0
  8. package/src/cli/build-auth.js +53 -0
  9. package/src/cli/build.js +40 -0
  10. package/src/cli/dev.js +12 -0
  11. package/src/cli/generate-api-pages.js +359 -0
  12. package/src/cli/init.js +81 -0
  13. package/src/cli/preflight.js +40 -0
  14. package/src/cli/run-astro.js +57 -0
  15. package/src/cli/run-pagefind.js +66 -0
  16. package/src/cli/write-redirects-file.js +80 -0
  17. package/src/components/Accordion.astro +164 -0
  18. package/src/components/AccordionGroup.astro +40 -0
  19. package/src/components/ApiLangSelect.astro +168 -0
  20. package/src/components/ApiPlayground.astro +281 -0
  21. package/src/components/ApiReferencePanel.astro +1754 -0
  22. package/src/components/ApiSchemaField.astro +54 -0
  23. package/src/components/AppIcon.astro +32 -0
  24. package/src/components/Badge.astro +128 -0
  25. package/src/components/Callout.astro +168 -0
  26. package/src/components/Card.astro +136 -0
  27. package/src/components/CardGroup.astro +20 -0
  28. package/src/components/CodeGroup.astro +184 -0
  29. package/src/components/CopyPageMenu.astro +246 -0
  30. package/src/components/Danger.astro +12 -0
  31. package/src/components/Expandable.astro +126 -0
  32. package/src/components/Frame.astro +102 -0
  33. package/src/components/Hint.astro +99 -0
  34. package/src/components/Icon.astro +70 -0
  35. package/src/components/Image.astro +147 -0
  36. package/src/components/Info.astro +12 -0
  37. package/src/components/Note.astro +12 -0
  38. package/src/components/Parameter.astro +119 -0
  39. package/src/components/RequestExample.astro +33 -0
  40. package/src/components/ResponseExample.astro +19 -0
  41. package/src/components/Searchbar.astro +117 -0
  42. package/src/components/Step.astro +10 -0
  43. package/src/components/Steps.astro +32 -0
  44. package/src/components/Tab.astro +9 -0
  45. package/src/components/Tabs.astro +52 -0
  46. package/src/components/Tip.astro +12 -0
  47. package/src/components/Video.astro +135 -0
  48. package/src/components/Warning.astro +12 -0
  49. package/src/components/index.ts +48 -0
  50. package/src/content.config.ts +223 -0
  51. package/src/layout/BaseLayout.astro +750 -0
  52. package/src/layout/components/AnalyticsScripts.astro +77 -0
  53. package/src/layout/components/AskAiWidget.astro +37 -0
  54. package/src/layout/components/Breadcrumbs.astro +97 -0
  55. package/src/layout/components/ImageZoom.astro +19 -0
  56. package/src/layout/components/MobileMenu.astro +200 -0
  57. package/src/layout/components/NavTree.astro +351 -0
  58. package/src/layout/components/SearchModal.astro +42 -0
  59. package/src/layout/components/Sidebar.astro +122 -0
  60. package/src/layout/components/SiteFooter.astro +85 -0
  61. package/src/layout/components/TableOfContents.astro +117 -0
  62. package/src/layout/components/TopBar.astro +311 -0
  63. package/src/layout/styles/banner.css +44 -0
  64. package/src/layout/styles/base.css +234 -0
  65. package/src/layout/styles/dropdown.css +133 -0
  66. package/src/layout/styles/footer.css +108 -0
  67. package/src/layout/styles/image-zoom.css +50 -0
  68. package/src/layout/styles/mobile-menu.css +258 -0
  69. package/src/layout/styles/search-modal.css +122 -0
  70. package/src/layout/styles/topbar.css +437 -0
  71. package/src/lib/config.ts +2131 -0
  72. package/src/lib/mdx-auto-hydrate.js +70 -0
  73. package/src/lib/mdx-inject-builtins.js +87 -0
  74. package/src/lib/mdx-substitute-variables.js +66 -0
  75. package/src/lib/mdx-title-anchor-ids.js +84 -0
  76. package/src/lib/mermaid-rehype.js +72 -0
  77. package/src/lib/openapi-render.ts +479 -0
  78. package/src/lib/shiki-code-block.js +102 -0
  79. package/src/lib/shiki-copy-button.js +45 -0
  80. package/src/lib/styles-asset-integration.js +210 -0
  81. package/src/lib/writedocs-temp-dir.js +93 -0
  82. package/src/pages/404.astro +62 -0
  83. package/src/pages/[...slug].astro +1270 -0
  84. package/src/pages/[...slug].md.ts +78 -0
  85. package/src/pages/llms-full.txt.ts +71 -0
  86. package/src/pages/llms.txt.ts +141 -0
  87. package/src/scripts/banner.ts +20 -0
  88. package/src/scripts/dropdowns.ts +61 -0
  89. package/src/scripts/image-zoom.ts +66 -0
  90. package/src/scripts/mobile-menu.ts +55 -0
  91. package/src/scripts/search.ts +155 -0
  92. package/src/scripts/sidebar-scroll.ts +65 -0
  93. package/src/scripts/theme-toggle.ts +35 -0
  94. package/src/scripts/topbar-offset.ts +141 -0
  95. package/src/styles/global.css +18 -0
@@ -0,0 +1,54 @@
1
+ ---
2
+ // Recursive renderer for a Body/Response schema tree (SchemaRow[] from
3
+ // openapi-render.ts's schemaRows()) - one .wd-api-param row per field,
4
+ // same name/type/required markup and CSS as ApiPlayground.astro's own
5
+ // path/query/header parameter rows (those classes live in that file's
6
+ // global <style> block, not duplicated here - this component only ever
7
+ // renders inside a page that already has ApiPlayground on it). A field
8
+ // with nested properties (an object, or an array of objects) wraps its
9
+ // own children in an <Expandable> - the same collapsible "Show/Hide
10
+ // properties" control already used for hand-written Parameter/Expandable
11
+ // content elsewhere on API pages (see Expandable.astro's own comment) -
12
+ // rather than the flat, permanently-open, depth-indented list this used
13
+ // to be. Collapsed by default (defaultOpen={false}, overriding
14
+ // Expandable's own open-by-default) - a schema with several levels of
15
+ // nesting (kitchen-sink's price/dimensions/components/manufacturer
16
+ // example) reads better as "click to see what's inside" than as a wall
17
+ // of every field at every depth shown at once. `<Astro.self>` recurses
18
+ // the same way NavTree.astro's own nested sidebar groups do - same
19
+ // established pattern in this codebase for "a tree of unknown depth, one
20
+ // component handles every level."
21
+ import Expandable from './Expandable.astro';
22
+ import type { SchemaRow } from '../lib/openapi-render';
23
+
24
+ interface Props {
25
+ rows: SchemaRow[];
26
+ // Request-body fields show a "required" pill (a caller genuinely needs
27
+ // to know); response-body fields never did, even before this component
28
+ // existed (a response shape doesn't have optional-vs-required in the
29
+ // same actionable sense a request payload does) - kept as an explicit
30
+ // opt-out rather than dropped, so ApiPlayground.astro's two call sites
31
+ // (request body, response body) can each keep their own prior behavior
32
+ // through this one shared, recursive renderer.
33
+ showRequired?: boolean;
34
+ }
35
+ const { rows, showRequired = true } = Astro.props as Props;
36
+ ---
37
+
38
+ {
39
+ rows.map((row) => (
40
+ <div class="wd-api-param">
41
+ <div class="wd-api-param-head">
42
+ <span class="wd-api-param-name">{row.name}</span>
43
+ <span class="wd-api-pill wd-api-pill-type">{row.type}</span>
44
+ {showRequired && row.required && <span class="wd-api-pill wd-api-pill-required">required</span>}
45
+ </div>
46
+ {row.description && <p class="wd-api-param-desc">{row.description}</p>}
47
+ {row.children.length > 0 && (
48
+ <Expandable title={`${row.name} properties`} defaultOpen={false}>
49
+ <Astro.self rows={row.children} showRequired={showRequired} />
50
+ </Expandable>
51
+ )}
52
+ </div>
53
+ ))
54
+ }
@@ -0,0 +1,32 @@
1
+ ---
2
+ // Renders any writedocs.json `icon` string, whichever of the two forms it
3
+ // takes (see resolveIcon() in lib/config.ts): an Iconify icon id (looked
4
+ // up against whatever @iconify-json/* collections are installed - see
5
+ // astro.config.mjs) rendered as a real <svg>, or literal text/emoji
6
+ // rendered as-is. Centralizes the icon/text branch so BaseLayout.astro
7
+ // and Card.astro (the two places writedocs.json icon strings get rendered)
8
+ // don't each need their own copy of the resolveIcon() call + branch.
9
+ import { Icon } from 'astro-icon/components';
10
+ import { resolveIcon } from '../lib/config';
11
+
12
+ interface Props {
13
+ icon?: string;
14
+ class?: string;
15
+ // Optional inline style passthrough - Icon.astro is the one real
16
+ // consumer, needing this to apply its own per-instance `size`/`color`
17
+ // props (arbitrary values, not a fixed set of classes like every
18
+ // other AppIcon call site uses). Left off Props' required list and
19
+ // undefined by default so every existing caller (BaseLayout, Card,
20
+ // Badge) is unaffected.
21
+ style?: string;
22
+ }
23
+ const { icon, class: className = 'wd-icon', style } = Astro.props as Props;
24
+ const resolved = icon ? resolveIcon(icon) : null;
25
+ ---
26
+ {resolved && (
27
+ resolved.kind === 'iconify' ? (
28
+ <Icon name={resolved.name} class={className} style={style} />
29
+ ) : (
30
+ <span class={className} style={style}>{resolved.value}</span>
31
+ )
32
+ )}
@@ -0,0 +1,128 @@
1
+ ---
2
+ // A small inline label - status indicators, version tags, "Beta"/"New"
3
+ // markers - modeled on Mintlify's own Badge
4
+ // (https://www.mintlify.com/docs/components/badge). Renders as an
5
+ // inline <span> (not a block element) so it drops naturally into a
6
+ // sentence, the same way <Hint> does, rather than needing its own line.
7
+ //
8
+ // Colors are a fixed semantic set (not arbitrary hex) using the same
9
+ // --wd-*-accent CSS-custom-property pattern Callout.astro's own
10
+ // .wd-callout-note/-info/-tip/-warning/-danger classes already
11
+ // establish - one shared color per class name, everything else in the
12
+ // component reads it via var(). Mintlify's own color list additionally
13
+ // has white/surface/white-destructive/surface-destructive variants,
14
+ // tied to their specific light/dark neutral-surface tokens; left out
15
+ // here since they don't map onto this site's own (simpler) light/dark
16
+ // token set, and "gray" already covers the neutral case.
17
+ import AppIcon from './AppIcon.astro';
18
+
19
+ interface Props {
20
+ color?: 'gray' | 'blue' | 'green' | 'yellow' | 'orange' | 'red' | 'purple';
21
+ size?: 'xs' | 'sm' | 'md' | 'lg';
22
+ shape?: 'rounded' | 'pill';
23
+ icon?: string;
24
+ stroke?: boolean;
25
+ disabled?: boolean;
26
+ }
27
+ const { color = 'gray', size = 'md', shape = 'rounded', icon, stroke = false, disabled = false } =
28
+ Astro.props as Props;
29
+
30
+ const KNOWN_COLORS = ['gray', 'blue', 'green', 'yellow', 'orange', 'red', 'purple'];
31
+ const colorClass = KNOWN_COLORS.includes(color) ? color : 'gray';
32
+ const KNOWN_SIZES = ['xs', 'sm', 'md', 'lg'];
33
+ const sizeClass = KNOWN_SIZES.includes(size) ? size : 'md';
34
+ const shapeClass = shape === 'pill' ? 'pill' : 'rounded';
35
+ ---
36
+
37
+ <span
38
+ class:list={[
39
+ 'wd-badge',
40
+ `wd-badge-${colorClass}`,
41
+ `wd-badge-${sizeClass}`,
42
+ `wd-badge-${shapeClass}`,
43
+ stroke && 'wd-badge-stroke',
44
+ disabled && 'wd-badge-disabled',
45
+ ]}
46
+ >
47
+ {icon && <AppIcon icon={icon} class="wd-badge-icon" />}<slot />
48
+ </span>
49
+ <style>
50
+ .wd-badge {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ gap: 0.35em;
54
+ line-height: 1;
55
+ font-weight: 600;
56
+ white-space: nowrap;
57
+ vertical-align: middle;
58
+ border: 1px solid transparent;
59
+ /* Filled by default - a tint of the accent color over the page
60
+ background, with the accent itself as the text color. */
61
+ background: color-mix(in srgb, var(--wd-badge-accent) 14%, var(--wd-background));
62
+ color: var(--wd-badge-accent);
63
+ }
64
+ /* :global() is load-bearing here, same reason as Card.astro's own
65
+ identical comment on its .wd-card-icon rules: AppIcon.astro
66
+ renders this span/svg, not Badge.astro, so it never carries
67
+ Badge's own scope attribute. Sized in em so it scales with
68
+ whichever size class is active without needing its own
69
+ xs/sm/md/lg variants. */
70
+ :global(.wd-badge-icon) {
71
+ display: block;
72
+ width: 1em;
73
+ height: 1em;
74
+ flex-shrink: 0;
75
+ }
76
+ .wd-badge-gray {
77
+ --wd-badge-accent: #64748b;
78
+ }
79
+ .wd-badge-blue {
80
+ --wd-badge-accent: #3b82f6;
81
+ }
82
+ .wd-badge-green {
83
+ --wd-badge-accent: #16a34a;
84
+ }
85
+ .wd-badge-yellow {
86
+ --wd-badge-accent: #ca8a04;
87
+ }
88
+ .wd-badge-orange {
89
+ --wd-badge-accent: #d97706;
90
+ }
91
+ .wd-badge-red {
92
+ --wd-badge-accent: #dc2626;
93
+ }
94
+ .wd-badge-purple {
95
+ --wd-badge-accent: #9333ea;
96
+ }
97
+ /* Outline instead of filled - a subtler treatment for the same
98
+ color, per Mintlify's own "stroke" prop. */
99
+ .wd-badge-stroke {
100
+ background: transparent;
101
+ border-color: var(--wd-badge-accent);
102
+ }
103
+ .wd-badge-rounded {
104
+ border-radius: 0.35em;
105
+ }
106
+ .wd-badge-pill {
107
+ border-radius: 999px;
108
+ }
109
+ .wd-badge-xs {
110
+ padding: 0.05em 0.45em;
111
+ font-size: 0.65rem;
112
+ }
113
+ .wd-badge-sm {
114
+ padding: 0.1em 0.55em;
115
+ font-size: 0.72rem;
116
+ }
117
+ .wd-badge-md {
118
+ padding: 0.15em 0.65em;
119
+ font-size: 0.8rem;
120
+ }
121
+ .wd-badge-lg {
122
+ padding: 0.25em 0.8em;
123
+ font-size: 0.92rem;
124
+ }
125
+ .wd-badge-disabled {
126
+ opacity: 0.5;
127
+ }
128
+ </style>
@@ -0,0 +1,168 @@
1
+ ---
2
+ // See src/components/{Note,Tip,Warning,Danger,Info}.astro - one thin
3
+ // wrapper per `type` below, each just `<Callout type="x">` under the
4
+ // hood, so a page can write either `<Callout type="info">` or the
5
+ // shorthand `<Info>` and get an identical result. Both forms take the
6
+ // same `title` prop.
7
+ import AppIcon from "./AppIcon.astro";
8
+
9
+ interface Props {
10
+ type?: "note" | "tip" | "warning" | "danger" | "info";
11
+ title?: string;
12
+ // Set automatically by remarkCalloutAnchorIds (src/lib/mdx-callout-anchor-ids.js)
13
+ // for every <Callout title="...">/<Note title="...">/etc. it finds in an
14
+ // .mdx file's own JSX, already deduped (-2, -3, ...) against every other
15
+ // callout title on that same page - see that plugin's own comment for
16
+ // why the dedup has to happen there, one level up, rather than here.
17
+ // Not meant to be set by hand in content; the `slugify(title)` fallback
18
+ // below covers every case this doesn't reach (plain .md content, a
19
+ // dynamic `title={expr}`, or this component used directly from a .astro
20
+ // file rather than through MDX).
21
+ _titleId?: string;
22
+ }
23
+ const { type = "note", title, _titleId } = Astro.props as Props;
24
+ // FontAwesome Solid (fa6-solid, already an installed @iconify-json/*
25
+ // collection - see astro.config.mjs's own comment on icon() needing no
26
+ // allowlist) rather than a literal emoji, so every callout gets a real,
27
+ // theme-colored <svg> instead of a platform-dependent glyph.
28
+ const icons: Record<string, string> = {
29
+ note: "fa6-solid:note-sticky",
30
+ info: "fa6-solid:circle-info",
31
+ tip: "fa6-solid:lightbulb",
32
+ warning: "fa6-solid:triangle-exclamation",
33
+ danger: "fa6-solid:circle-xmark",
34
+ };
35
+ // Astro's own heading-id slugging (github-slugger, via the markdown
36
+ // pipeline) never sees this - `title` is a component prop, not a
37
+ // markdown heading, so it gets no id unless we make one ourselves.
38
+ function slugify(value: string): string {
39
+ return value
40
+ .toLowerCase()
41
+ .replace(/[^a-z0-9]+/g, "-")
42
+ .replace(/^-+|-+$/g, "");
43
+ }
44
+ const titleId = title ? (_titleId ?? slugify(title)) : undefined;
45
+ ---
46
+
47
+ <div class:list={["wd-callout", `wd-callout-${type}`, { "wd-callout-titled": Boolean(title) }]}>
48
+ <AppIcon icon={icons[type]} class="wd-callout-icon" />
49
+ <div class="wd-callout-content">
50
+ {
51
+ title && (
52
+ <div class="wd-callout-title" id={titleId}>
53
+ {title}
54
+ <a class="wd-heading-anchor" href={`#${titleId}`} aria-label="Link to this section">
55
+ #
56
+ </a>
57
+ </div>
58
+ )
59
+ }
60
+ <div class="wd-callout-body"><slot /></div>
61
+ </div>
62
+ </div>
63
+ <style>
64
+ .wd-callout {
65
+ display: flex;
66
+ gap: 0.7rem;
67
+ padding: 0.85rem 1.1rem;
68
+ border-radius: 0.6rem;
69
+ border-left: 3px solid var(--wd-callout-accent);
70
+ background: color-mix(in srgb, var(--wd-callout-accent) 10%, var(--wd-background));
71
+ margin: 1.25rem 0;
72
+ }
73
+ /* A titled callout has two stacked lines (title, then body) - the icon
74
+ should align with the title's cap-height, not float centered across
75
+ the whole two-line block the way it does in the single-line,
76
+ untitled case above. */
77
+ .wd-callout.wd-callout-titled {
78
+ padding-block: 1rem;
79
+ }
80
+ /* :global() is load-bearing on every rule below, not just this one -
81
+ AppIcon.astro renders the actual <svg class="wd-callout-icon">, not
82
+ Callout.astro, so none of these ever carry Callout's own scope
83
+ attribute. A selector like `.wd-callout-icon { color: ... }` with
84
+ no :global() compiles to `.wd-callout-icon[data-astro-cid-x]`,
85
+ which can never match that svg - it silently does nothing rather
86
+ than erroring, which is exactly what happened here the first time
87
+ around (icon rendered at the right size, via the one rule that
88
+ *was* already global, but stayed un-colored black - the default
89
+ SVG fill - since its color rule was scoped and never matched).
90
+ Same root cause Card.astro's own icon rule already works around. */
91
+ :global(svg.wd-callout-icon) {
92
+ display: block;
93
+ flex-shrink: 0;
94
+ width: 1.15rem;
95
+ height: 1.15rem;
96
+ color: var(--wd-callout-accent);
97
+ fill: currentColor;
98
+ margin-top: 0.2rem;
99
+ }
100
+ .wd-callout-content {
101
+ flex: 1;
102
+ min-width: 0;
103
+ }
104
+ /* --wd-topbar-offset is a custom property set on <html> by
105
+ initTopbarOffset() (src/scripts/topbar-offset.ts) - inherits down to
106
+ here like any other CSS var regardless of Astro's scoping (that only
107
+ affects selector matching, not the ordinary property cascade), so
108
+ this reads the exact same live value [...slug].astro's own h2/h3/h4
109
+ scroll-margin-top rules do. Without it, jumping to a #callout-title
110
+ anchor landed the callout right at the viewport's true top edge,
111
+ partly hidden under the sticky topbar - the same bug those heading
112
+ rules exist to prevent, just missing here since this id didn't exist
113
+ until the anchor feature above was added. The extra flat + 1rem on
114
+ top of the measured topbar height (headings don't need this - a
115
+ heading's own text sits flush at its scroll target; a callout's
116
+ title sits inside the box's own 1rem top padding, `padding-block: 1rem`
117
+ on .wd-callout-titled above, so landing at exactly the topbar's
118
+ height alone still let that padding strip tuck under it, reading as
119
+ "still a bit cut off") is breathing room, not a topbar measurement -
120
+ it doesn't need to come from the live-measured var itself. */
121
+ .wd-callout-title {
122
+ font-weight: 700;
123
+ color: var(--wd-callout-accent);
124
+ margin-bottom: 0.3rem;
125
+ scroll-margin-top: calc(var(--wd-topbar-offset, 5rem) + 1rem);
126
+ }
127
+ /* .wd-heading-anchor's base look (color, opacity: 0, transition) comes
128
+ from [...slug].astro's own <style is:global> block - it already
129
+ applies anywhere that class shows up, headings or not. Only the
130
+ hover trigger needs to live here, and unlike the icon rules above,
131
+ no :global() is needed for it: both .wd-callout-title and the
132
+ anchor inside it are rendered directly by this component's own
133
+ template, so they both carry the same scope attribute and this
134
+ plain selector matches correctly as-is. */
135
+ .wd-callout-title:hover .wd-heading-anchor {
136
+ opacity: 1;
137
+ }
138
+ .wd-callout-body {
139
+ color: var(--wd-text);
140
+ }
141
+ .wd-callout-body :global(p:first-child) {
142
+ margin-top: 0;
143
+ }
144
+ .wd-callout-body :global(p:last-child) {
145
+ margin-bottom: 0;
146
+ }
147
+
148
+ /* One accent color per type, in both light and dark mode - the
149
+ background/border/icon/title all derive from this single value via
150
+ color-mix() against --wd-background (which already resolves
151
+ correctly per [data-theme] on its own), so there's no separate
152
+ [data-theme='dark'] override block needed here at all. */
153
+ .wd-callout-note {
154
+ --wd-callout-accent: #64748b;
155
+ }
156
+ .wd-callout-info {
157
+ --wd-callout-accent: #3b82f6;
158
+ }
159
+ .wd-callout-tip {
160
+ --wd-callout-accent: #16a34a;
161
+ }
162
+ .wd-callout-warning {
163
+ --wd-callout-accent: #d97706;
164
+ }
165
+ .wd-callout-danger {
166
+ --wd-callout-accent: #dc2626;
167
+ }
168
+ </style>
@@ -0,0 +1,136 @@
1
+ ---
2
+ import AppIcon from './AppIcon.astro';
3
+
4
+ // Three visual variants, chosen by which optional prop is set - never more
5
+ // than one of icon/img/method actually renders at once, but they're kept
6
+ // as independent props (not a single `variant` union) since `icon` is
7
+ // meant to keep working exactly as before for existing content, and `href`
8
+ // applies identically to all three.
9
+ interface Props {
10
+ title: string;
11
+ icon?: string;
12
+ // Renders a full-width image above the title instead of AppIcon's small
13
+ // icon - takes priority over `icon` if both are somehow set (see the
14
+ // template below), since a real image is the more deliberate/specific
15
+ // choice of the two.
16
+ img?: string;
17
+ // API-reference style: a solid, colored method badge next to the title,
18
+ // top-right. Same 5 known methods + fallback-gray "other" as the
19
+ // sidebar's own OpenAPI badges (NavTree.astro's wd-nav-method-*) and the
20
+ // Try-it playground's header (ApiPlayground.astro's wd-api-method-*) -
21
+ // this is a third, independent copy of that same get/post/put/patch/
22
+ // delete color set rather than a shared import, consistent with how
23
+ // those two already don't share one either (different visual treatment
24
+ // each - tinted pill in the sidebar, solid pill in the playground, solid
25
+ // pill here too but sized for a card heading, not worth forcing into one
26
+ // generic component over a handful of literal hex values).
27
+ method?: string;
28
+ href?: string;
29
+ }
30
+ const { title, icon, img, method, href } = Astro.props as Props;
31
+ const Tag = href ? 'a' : 'div';
32
+ const KNOWN_METHODS = ['get', 'post', 'put', 'patch', 'delete'];
33
+ const methodLower = method?.toLowerCase();
34
+ const methodClass = methodLower && KNOWN_METHODS.includes(methodLower) ? methodLower : 'other';
35
+ ---
36
+ <Tag class="wd-card" href={href}>
37
+ {img && <img src={img} alt="" class="wd-card-image" />}
38
+ {!img && <AppIcon icon={icon} class="wd-card-icon" />}
39
+ <div class="wd-card-title-row">
40
+ <h3 class="wd-card-title">{title}</h3>
41
+ {
42
+ method && (
43
+ <span class:list={['wd-card-method', `wd-card-method-${methodClass}`]}>
44
+ {method.toUpperCase()}
45
+ </span>
46
+ )
47
+ }
48
+ </div>
49
+ <div class="wd-card-body"><slot /></div>
50
+ </Tag>
51
+ <style>
52
+ .wd-card {
53
+ display: block;
54
+ border: 1px solid var(--wd-border);
55
+ border-radius: 0.6rem;
56
+ padding: 1.1rem 1.2rem;
57
+ text-decoration: none;
58
+ color: inherit;
59
+ transition: border-color 0.15s ease;
60
+ }
61
+ a.wd-card:hover {
62
+ border-color: var(--wd-primary);
63
+ }
64
+ /* :global() is load-bearing: AppIcon.astro renders this span/svg, not
65
+ Card.astro, so it never carries Card's own scope attribute - without
66
+ :global() these selectors compile to something that can never match. */
67
+ :global(span.wd-card-icon) {
68
+ display: block;
69
+ font-size: 1.3rem;
70
+ }
71
+ :global(svg.wd-card-icon) {
72
+ display: block;
73
+ width: 1.3rem;
74
+ height: 1.3rem;
75
+ }
76
+ .wd-card-image {
77
+ display: block;
78
+ width: 100%;
79
+ height: 9rem;
80
+ object-fit: cover;
81
+ border-radius: 0.4rem;
82
+ background: var(--wd-surface);
83
+ }
84
+ /* Same 0.4rem/0.3rem the title itself used to carry directly, moved up
85
+ onto this wrapper so it stays correct whether the row holds just a
86
+ title (icon/img/no-icon variants) or a title + method badge side by
87
+ side - a flex row's own margin does the same job a block element's
88
+ margin did, without changing how any existing (no-method) card looks. */
89
+ .wd-card-title-row {
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: space-between;
93
+ gap: 0.5rem;
94
+ margin: 0.4rem 0 0.3rem;
95
+ }
96
+ .wd-card-title {
97
+ margin: 0;
98
+ font-size: 1rem;
99
+ }
100
+ .wd-card-method {
101
+ flex-shrink: 0;
102
+ display: inline-block;
103
+ padding: 0.2rem 0.55rem;
104
+ border-radius: 0.3rem;
105
+ font-size: 0.72rem;
106
+ font-weight: 700;
107
+ letter-spacing: 0.02em;
108
+ color: #fff;
109
+ }
110
+ .wd-card-method-get {
111
+ background: #16a34a;
112
+ }
113
+ .wd-card-method-post {
114
+ background: #2563eb;
115
+ }
116
+ .wd-card-method-put {
117
+ background: #9333ea;
118
+ }
119
+ .wd-card-method-patch {
120
+ background: #d97706;
121
+ }
122
+ .wd-card-method-delete {
123
+ background: #dc2626;
124
+ }
125
+ .wd-card-method-other {
126
+ background: var(--wd-text-muted);
127
+ }
128
+ .wd-card-body {
129
+ font-size: 0.9rem;
130
+ color: var(--wd-text-muted);
131
+ margin: 0;
132
+ }
133
+ .wd-card-body :global(p) {
134
+ margin: 0;
135
+ }
136
+ </style>
@@ -0,0 +1,20 @@
1
+ ---
2
+ interface Props {
3
+ cols?: number;
4
+ }
5
+ const { cols = 2 } = Astro.props as Props;
6
+ ---
7
+ <div class="wd-card-group" style={`--wd-cols: ${cols}`}>
8
+ <slot />
9
+ </div>
10
+ <style>
11
+ .wd-card-group {
12
+ display: grid;
13
+ grid-template-columns: repeat(var(--wd-cols), minmax(0, 1fr));
14
+ gap: 1rem;
15
+ margin: 1.25rem 0;
16
+ }
17
+ @media (max-width: 640px) {
18
+ .wd-card-group { grid-template-columns: 1fr; }
19
+ }
20
+ </style>