@dogsbay/ui 0.2.0-beta.7 → 0.2.0-beta.72

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dogsbay/ui",
3
- "version": "0.2.0-beta.7",
3
+ "version": "0.2.0-beta.72",
4
4
  "description": "Accessible UI components for Astro, inspired by Base UI and shadcn",
5
5
  "type": "module",
6
6
  "exports": {
@@ -10,10 +10,10 @@
10
10
  "tailwind-variants": "^0.3.0"
11
11
  },
12
12
  "peerDependencies": {
13
- "astro": "^5.0.0 || ^6.0.0",
14
- "@dogsbay/primitives": "0.2.0-beta.7",
15
- "@dogsbay/elements": "0.2.0-beta.7",
16
- "@dogsbay/icons": "0.2.0-beta.7"
13
+ "astro": "^5.0.0 || ^6.0.0 || ^7.0.0",
14
+ "@dogsbay/primitives": "0.2.0-beta.72",
15
+ "@dogsbay/elements": "0.2.0-beta.72",
16
+ "@dogsbay/icons": "0.2.0-beta.72"
17
17
  },
18
18
  "optionalDependencies": {
19
19
  "shiki": "^4.0.0",
@@ -126,7 +126,7 @@ function inlineToHtml(nodes: InlineNode[]): string {
126
126
  case "code": return `<code>${n.text || ""}</code>`;
127
127
  case "highlight": return `<mark>${inlineToHtml((n.children || []) as InlineNode[])}</mark>`;
128
128
  case "link": return `<a href="${n.href || ""}">${inlineToHtml((n.children || []) as InlineNode[])}</a>`;
129
- case "image": return `<img src="${n.src || ""}" alt="${n.alt || ""}">`;
129
+ case "image": return `<img src="${withBaseUrl(n.src as string | undefined)}" alt="${n.alt || ""}">`;
130
130
  case "kbd": return (n.keys as string[] || []).map((k: string) => `<kbd>${k}</kbd>`).join("+");
131
131
  case "html-inline": return String(n.html || "");
132
132
  case "break": return "<br>";
@@ -142,6 +142,26 @@ function resolveImage(src: string): ImageMetadata | undefined {
142
142
  const clean = normalized.replace(/#.*$/, "");
143
143
  return images[clean];
144
144
  }
145
+
146
+ /**
147
+ * Prefix Astro's `base` config (= urlBase from site.url's path)
148
+ * onto absolute image paths so they resolve on subpath-mounted
149
+ * deploys. Skip for external / data: / already-prefixed URLs.
150
+ * See plans/content-assets-folder.md.
151
+ */
152
+ function withBaseUrl(src: string | undefined): string {
153
+ if (!src) return "";
154
+ const baseUrl = import.meta.env.BASE_URL.replace(/\/$/, "");
155
+ if (
156
+ !baseUrl ||
157
+ !src.startsWith("/") ||
158
+ src.startsWith(`${baseUrl}/`) ||
159
+ /^(?:[a-z]+:|\/\/)/i.test(src)
160
+ ) {
161
+ return src;
162
+ }
163
+ return `${baseUrl}${src}`;
164
+ }
145
165
  ---
146
166
 
147
167
  {/* ── Prose ──────────────────────────────────────────────── */}
@@ -296,11 +316,14 @@ function resolveImage(src: string): ImageMetadata | undefined {
296
316
  {node.type === "hr" && <hr class="my-8 border-border" />}
297
317
  {node.type === "html" && (() => {
298
318
  let html = node.html || "";
299
- if (html.includes("<img") && Object.keys(images).length > 0) {
319
+ if (html.includes("<img")) {
300
320
  html = html.replace(/src="([^"]+)"/g, (_match: string, src: string) => {
301
321
  const resolved = resolveImage(src);
302
322
  if (resolved) return `src="${resolved.src}" width="${resolved.width}" height="${resolved.height}"`;
303
- return _match;
323
+ // Not in the glob (SVG / PDF / build-without-optimization) —
324
+ // prefix BASE_URL so absolute paths resolve on subpath deploys.
325
+ const prefixed = withBaseUrl(src);
326
+ return prefixed === src ? _match : `src="${prefixed}"`;
304
327
  });
305
328
  }
306
329
  return <Fragment set:html={html} />;
@@ -47,6 +47,24 @@ function resolveImage(src: string): ImageMetadata | undefined {
47
47
 
48
48
  const resolvedImg = node.type === "image" ? resolveImage(node.src as string || "") : undefined;
49
49
 
50
+ // When the image isn't in the glob map (SVG, PDF, or built with
51
+ // imageOptimization: false), the `<img src={node.src}>` fallback
52
+ // fires below. Prefix Astro's `base` for absolute paths so they
53
+ // resolve on subpath-mounted deploys. See plans/content-assets-folder.md.
54
+ function withBaseUrl(src: string | undefined): string {
55
+ if (!src) return "";
56
+ const baseUrl = import.meta.env.BASE_URL.replace(/\/$/, "");
57
+ if (
58
+ !baseUrl ||
59
+ !src.startsWith("/") ||
60
+ src.startsWith(`${baseUrl}/`) ||
61
+ /^(?:[a-z]+:|\/\/)/i.test(src)
62
+ ) {
63
+ return src;
64
+ }
65
+ return `${baseUrl}${src}`;
66
+ }
67
+
50
68
  // Icon resolution
51
69
  const iconSvg = node.type === "icon" && icons
52
70
  ? icons.resolve(node.library || "emoji", node.name, node.variant, node.size) as string | null
@@ -90,7 +108,7 @@ const isEmojiIcon = node.type === "icon" && (node.library === "emoji" || (iconSv
90
108
  )}
91
109
  {node.type === "image" && !resolvedImg && (
92
110
  <img
93
- src={node.src}
111
+ src={withBaseUrl(node.src as string | undefined)}
94
112
  alt={node.alt || ""}
95
113
  title={node.title}
96
114
  width={node.width}
@@ -0,0 +1,84 @@
1
+ ---
2
+ /**
3
+ * Icon — render an icon by name from any Iconify pack (defaults
4
+ * to Lucide).
5
+ *
6
+ * Resolves at BUILD time via `@dogsbay/icons`'s registry — the
7
+ * SVG is inlined into the static HTML, so there's no client-side
8
+ * fetch and no FOUC. Tree-shakeable: only icons actually used in
9
+ * pages get inlined, since Astro statically evaluates the
10
+ * resolution per template instance.
11
+ *
12
+ * Usage:
13
+ *
14
+ * <Icon name="rocket" /> // bare → Lucide rocket
15
+ * <Icon name="lucide:book-open" /> // explicit pack
16
+ * <Icon name="mdi:home" /> // Material Design Icons
17
+ * <Icon name="simple-icons:github" /> // brand icons
18
+ * <Icon name="rocket" class="size-5 text-primary" />
19
+ *
20
+ * Bare names check the emoji map first (so `rocket` → 🚀
21
+ * matches existing inline-icon behaviour), then fall through to
22
+ * Lucide. When the icon doesn't exist anywhere, the component
23
+ * renders nothing — silent fallback rather than a broken-image
24
+ * box, since icons are decorative and consistent typography is
25
+ * preferable to a placeholder.
26
+ *
27
+ * Accessibility: when `alt` is supplied, the icon is announced
28
+ * to screen readers as a labelled image. Without `alt` (the
29
+ * default), the SVG is `aria-hidden` because most icons in this
30
+ * codebase are paired with adjacent text that already conveys
31
+ * the meaning. Pass `alt=""` explicitly to force the
32
+ * decorative-only state when needed by external tooling.
33
+ */
34
+ import { resolveIcon } from "@dogsbay/icons";
35
+
36
+ interface Props {
37
+ /**
38
+ * Icon shorthand. Either bare (`rocket`, `book-open`) or
39
+ * namespaced (`pack:name`). See file header for examples.
40
+ */
41
+ name: string;
42
+ /** Tailwind class string. Defaults to `size-4` for inline-text sizing. */
43
+ class?: string;
44
+ /**
45
+ * Accessible label. Set to a non-empty string for icons that
46
+ * convey information without surrounding text. Leave undefined
47
+ * (or pass empty string) for decorative icons paired with a
48
+ * text label.
49
+ */
50
+ alt?: string;
51
+ }
52
+
53
+ const { name, class: className = "size-4", alt } = Astro.props;
54
+ const svg = resolveIcon(name);
55
+ const labelled = typeof alt === "string" && alt.length > 0;
56
+ ---
57
+
58
+ {
59
+ svg ? (
60
+ svg.startsWith("<") ? (
61
+ <span
62
+ class:list={["inline-flex shrink-0", className]}
63
+ role={labelled ? "img" : undefined}
64
+ aria-label={labelled ? alt : undefined}
65
+ aria-hidden={labelled ? undefined : "true"}
66
+ set:html={svg}
67
+ data-icon={name}
68
+ />
69
+ ) : (
70
+ // Emoji fallback — `svg` is actually a Unicode character
71
+ // when the name resolved against the emoji map (e.g.
72
+ // `rocket` → 🚀). Render it as text so font emoji shows.
73
+ <span
74
+ class:list={["inline-flex shrink-0", className]}
75
+ role={labelled ? "img" : undefined}
76
+ aria-label={labelled ? alt : undefined}
77
+ aria-hidden={labelled ? undefined : "true"}
78
+ data-icon={name}
79
+ >
80
+ {svg}
81
+ </span>
82
+ )
83
+ ) : null
84
+ }
@@ -0,0 +1 @@
1
+ export { default as Icon } from "./Icon.astro";
@@ -47,6 +47,27 @@ const captionText = caption || title;
47
47
  const w = width || (imageData ? imageData.width : undefined);
48
48
  const h = height || (imageData ? imageData.height : undefined);
49
49
 
50
+ // When `imageData` is unavailable (image not in import.meta.glob —
51
+ // e.g. SVG, PDF, or build-time `imageOptimization: false`), the
52
+ // `<img src={src}>` fallback fires with the raw markdown src. For
53
+ // absolute paths like `/_assets/foo.png`, we need to prefix with
54
+ // Astro's `base` so the URL resolves on subpath-mounted deploys
55
+ // (GH Pages project pages, multi-mount Cloudflare).
56
+ //
57
+ // `<AstroImage>` does this automatically; raw `<img>` does not.
58
+ //
59
+ // Skip prefixing for: external URLs (http/https), data: URIs,
60
+ // and paths that already start with `import.meta.env.BASE_URL`
61
+ // (defensive; shouldn't normally happen).
62
+ const baseUrl = import.meta.env.BASE_URL.replace(/\/$/, "");
63
+ const resolvedSrc =
64
+ baseUrl &&
65
+ src.startsWith("/") &&
66
+ !src.startsWith(`${baseUrl}/`) &&
67
+ !/^(?:[a-z]+:|\/\/)/i.test(src)
68
+ ? `${baseUrl}${src}`
69
+ : src;
70
+
50
71
  const alignClass = align === "left"
51
72
  ? "float-left mr-4 mb-2"
52
73
  : align === "right"
@@ -71,7 +92,7 @@ const alignClass = align === "left"
71
92
  />
72
93
  ) : (
73
94
  <img
74
- src={src}
95
+ src={resolvedSrc}
75
96
  alt={alt}
76
97
  title={title}
77
98
  width={w}
@@ -102,7 +123,7 @@ const alignClass = align === "left"
102
123
  />
103
124
  ) : (
104
125
  <img
105
- src={src}
126
+ src={resolvedSrc}
106
127
  alt={alt}
107
128
  title={title}
108
129
  width={w}