@veluai/velu 0.2.13 → 0.2.15

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 (109) hide show
  1. package/README.md +80 -80
  2. package/dist/cli.js +37 -37
  3. package/package.json +64 -64
  4. package/runtime/velu-ui/base.css +320 -320
  5. package/runtime/velu-ui/components/Accordion.jsx +64 -64
  6. package/runtime/velu-ui/components/ApiClient.jsx +207 -207
  7. package/runtime/velu-ui/components/ApiField.jsx +87 -87
  8. package/runtime/velu-ui/components/ApiPath.jsx +63 -63
  9. package/runtime/velu-ui/components/ApiReferencePage.jsx +384 -384
  10. package/runtime/velu-ui/components/ApiSamples.jsx +36 -36
  11. package/runtime/velu-ui/components/ApiSidebar.jsx +122 -122
  12. package/runtime/velu-ui/components/AskBar.jsx +71 -71
  13. package/runtime/velu-ui/components/Callout.jsx +114 -114
  14. package/runtime/velu-ui/components/Card.jsx +131 -131
  15. package/runtime/velu-ui/components/Chatbot.jsx +885 -885
  16. package/runtime/velu-ui/components/CodeBlock.jsx +375 -375
  17. package/runtime/velu-ui/components/Columns.jsx +56 -56
  18. package/runtime/velu-ui/components/ContextMenu.jsx +298 -273
  19. package/runtime/velu-ui/components/ErrorCard.jsx +138 -138
  20. package/runtime/velu-ui/components/Field.jsx +81 -81
  21. package/runtime/velu-ui/components/Image.jsx +163 -163
  22. package/runtime/velu-ui/components/Logo.jsx +31 -31
  23. package/runtime/velu-ui/components/MethodBadge.jsx +31 -31
  24. package/runtime/velu-ui/components/NavSelect.jsx +108 -108
  25. package/runtime/velu-ui/components/NotFound.jsx +63 -63
  26. package/runtime/velu-ui/components/PageFeedback.jsx +219 -219
  27. package/runtime/velu-ui/components/PageFooter.jsx +145 -145
  28. package/runtime/velu-ui/components/PageHeader.jsx +422 -422
  29. package/runtime/velu-ui/components/PageNav.jsx +77 -77
  30. package/runtime/velu-ui/components/PoweredBy.jsx +51 -51
  31. package/runtime/velu-ui/components/Prompt.jsx +115 -115
  32. package/runtime/velu-ui/components/Search.jsx +460 -460
  33. package/runtime/velu-ui/components/Sidebar.jsx +254 -254
  34. package/runtime/velu-ui/components/SocialLinks.jsx +90 -90
  35. package/runtime/velu-ui/components/Steps.jsx +65 -65
  36. package/runtime/velu-ui/components/ThemeToggle.jsx +48 -48
  37. package/runtime/velu-ui/components/Toc.jsx +537 -537
  38. package/runtime/velu-ui/components/TocBar.jsx +195 -195
  39. package/runtime/velu-ui/components/Tree.jsx +87 -87
  40. package/runtime/velu-ui/components/TryItBar.jsx +102 -102
  41. package/runtime/velu-ui/components/accordion.css +92 -92
  42. package/runtime/velu-ui/components/api-page.css +208 -208
  43. package/runtime/velu-ui/components/api.css +635 -635
  44. package/runtime/velu-ui/components/ask-bar.css +94 -94
  45. package/runtime/velu-ui/components/card.css +105 -105
  46. package/runtime/velu-ui/components/chatbot.css +622 -622
  47. package/runtime/velu-ui/components/code-block.css +263 -263
  48. package/runtime/velu-ui/components/context-menu.css +173 -173
  49. package/runtime/velu-ui/components/docs-layout.css +822 -822
  50. package/runtime/velu-ui/components/field.css +82 -82
  51. package/runtime/velu-ui/components/image.css +237 -237
  52. package/runtime/velu-ui/components/nav-select.css +157 -157
  53. package/runtime/velu-ui/components/not-found.css +94 -94
  54. package/runtime/velu-ui/components/page-feedback.css +241 -241
  55. package/runtime/velu-ui/components/page-footer.css +130 -130
  56. package/runtime/velu-ui/components/page-header.css +558 -558
  57. package/runtime/velu-ui/components/page-nav.css +50 -50
  58. package/runtime/velu-ui/components/powered-by.css +92 -92
  59. package/runtime/velu-ui/components/prompt.css +99 -99
  60. package/runtime/velu-ui/components/search.css +307 -307
  61. package/runtime/velu-ui/components/sidebar.css +205 -205
  62. package/runtime/velu-ui/components/steps.css +77 -77
  63. package/runtime/velu-ui/components/theme-toggle.css +102 -102
  64. package/runtime/velu-ui/components/toc-bar.css +234 -234
  65. package/runtime/velu-ui/components/tree.css +49 -49
  66. package/runtime/velu-ui/index.js +54 -54
  67. package/runtime/velu-ui/lib/api-send.js +92 -92
  68. package/runtime/velu-ui/lib/brand-icons.jsx +103 -103
  69. package/runtime/velu-ui/lib/component-schemas.js +100 -100
  70. package/runtime/velu-ui/lib/copyText.js +64 -64
  71. package/runtime/velu-ui/lib/docs-assistant.js +250 -250
  72. package/runtime/velu-ui/lib/lang-icons.jsx +147 -147
  73. package/runtime/velu-ui/lib/pagefind.js +113 -113
  74. package/runtime/velu-ui/lib/prism-langs.js +957 -957
  75. package/runtime/velu-ui/lib/prism-loader.js +74 -74
  76. package/runtime/velu-ui/lib/resolveIcon.jsx +29 -29
  77. package/runtime/velu-ui/lib/scrollIntoNearestView.js +66 -66
  78. package/runtime/velu-ui/mdx-components.jsx +105 -105
  79. package/runtime/velu-ui/primitives/Cluster.jsx +49 -49
  80. package/runtime/velu-ui/primitives/Stack.jsx +63 -63
  81. package/runtime/velu-ui/primitives/Switcher.jsx +57 -57
  82. package/runtime/velu-ui/primitives/stack.css +3 -3
  83. package/runtime/velu-ui/primitives/switcher.css +25 -25
  84. package/runtime/velu-ui/styles.css +46 -46
  85. package/runtime/velu-ui/tokens.css +4 -4
  86. package/schema/velu.schema.json +423 -423
  87. package/src/lib/extract-mdx-error.js +170 -170
  88. package/src/lib/issues.js +159 -159
  89. package/src/lib/known-components.js +34 -34
  90. package/src/navigation.js +443 -443
  91. package/src/runtime/App.jsx +1669 -1668
  92. package/src/runtime/ErrorBoundary.jsx +54 -54
  93. package/src/runtime/client-entry.jsx +22 -22
  94. package/src/runtime/server-entry.jsx +16 -16
  95. package/src/template.html +48 -48
  96. package/templates/starter/ai-tools/claude-code.mdx +26 -26
  97. package/templates/starter/ai-tools/cursor.mdx +17 -17
  98. package/templates/starter/api-reference/introduction.mdx +43 -43
  99. package/templates/starter/development.mdx +19 -19
  100. package/templates/starter/essentials/code.mdx +29 -29
  101. package/templates/starter/essentials/images.mdx +29 -29
  102. package/templates/starter/essentials/markdown.mdx +25 -25
  103. package/templates/starter/essentials/navigation.mdx +39 -39
  104. package/templates/starter/essentials/settings.mdx +30 -30
  105. package/templates/starter/favicon.svg +6 -6
  106. package/templates/starter/index.mdx +31 -31
  107. package/templates/starter/openapi.json +160 -160
  108. package/templates/starter/quickstart.mdx +31 -31
  109. package/templates/starter/velu.json +41 -41
@@ -1,90 +1,90 @@
1
- import React from 'react';
2
- import Cluster from '../primitives/Cluster.jsx';
3
-
4
- /**
5
- * SocialLinks — a row of social-platform icon links. Shared by the full
6
- * PageFooter (centered at its base) and the compact content-foot row (next to
7
- * "Powered by Velu" when there are no footer link columns).
8
- *
9
- * `socials` is `[{ kind, href }]`. Unknown kinds (no icon) are skipped.
10
- *
11
- * @param {{ socials?: {kind: string, href: string}[],
12
- * justify?: string, className?: string }} props
13
- */
14
-
15
- /* Inline SVGs, fill: currentColor so they inherit the themed text color. */
16
- function GithubIcon() {
17
- return (
18
- <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
19
- <path
20
- fillRule="evenodd"
21
- clipRule="evenodd"
22
- d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z"
23
- />
24
- </svg>
25
- );
26
- }
27
- function XIcon() {
28
- return (
29
- <svg viewBox="0 0 1200 1227" fill="currentColor" aria-hidden="true">
30
- <path d="M714.163 519.284 1160.89 0h-105.86L667.137 450.887 357.328 0H0l468.492 681.821L0 1226.37h105.866l409.625-476.152 327.181 476.152H1200L714.137 519.284h.026ZM569.165 687.828l-47.468-67.894-377.686-540.24h162.604l304.797 435.991 47.468 67.894 396.2 566.721H892.476L569.165 687.854v-.026Z" />
31
- </svg>
32
- );
33
- }
34
- function YoutubeIcon() {
35
- return (
36
- <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
37
- <path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
38
- </svg>
39
- );
40
- }
41
- function LinkedinIcon() {
42
- return (
43
- <svg viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
44
- <path d="M218.123 218.127h-37.931v-59.403c0-14.165-.253-32.4-19.728-32.4-19.756 0-22.779 15.434-22.779 31.369v60.43h-37.93V95.967h36.413v16.694h.51a39.907 39.907 0 0 1 35.928-19.733c38.445 0 45.533 25.288 45.533 58.186l-.016 67.013ZM56.955 79.27c-12.157.002-22.014-9.852-22.016-22.009-.002-12.157 9.851-22.014 22.008-22.016 12.157-.003 22.014 9.851 22.016 22.008A22.013 22.013 0 0 1 56.955 79.27m18.966 138.858H37.95V95.967h37.97v122.16ZM237.033.018H18.89C8.58-.098.125 8.161-.001 18.471v219.053c.122 10.315 8.576 18.582 18.89 18.474h218.144c10.336.128 18.823-8.139 18.966-18.474V18.454c-.147-10.33-8.635-18.588-18.966-18.453" />
45
- </svg>
46
- );
47
- }
48
-
49
- export const SOCIAL_ICONS = {
50
- github: GithubIcon,
51
- x: XIcon,
52
- youtube: YoutubeIcon,
53
- linkedin: LinkedinIcon,
54
- };
55
- const SOCIAL_LABELS = {
56
- github: 'GitHub',
57
- x: 'X',
58
- youtube: 'YouTube',
59
- linkedin: 'LinkedIn',
60
- };
61
-
62
- export default function SocialLinks({ socials = [], justify, className = '' }) {
63
- const known = socials.filter((s) => s && SOCIAL_ICONS[s.kind]);
64
- if (!known.length) return null;
65
- return (
66
- <Cluster
67
- space="var(--s-2)"
68
- align="center"
69
- justify={justify}
70
- className={`velu-socials ${className}`.trim()}
71
- >
72
- {known.map((s, i) => {
73
- const Icon = SOCIAL_ICONS[s.kind];
74
- return (
75
- <a
76
- key={i}
77
- href={s.href}
78
- target="_blank"
79
- rel="noreferrer"
80
- aria-label={SOCIAL_LABELS[s.kind] ?? s.kind}
81
- className="velu-social"
82
- data-kind={s.kind}
83
- >
84
- <Icon />
85
- </a>
86
- );
87
- })}
88
- </Cluster>
89
- );
90
- }
1
+ import React from 'react';
2
+ import Cluster from '../primitives/Cluster.jsx';
3
+
4
+ /**
5
+ * SocialLinks — a row of social-platform icon links. Shared by the full
6
+ * PageFooter (centered at its base) and the compact content-foot row (next to
7
+ * "Powered by Velu" when there are no footer link columns).
8
+ *
9
+ * `socials` is `[{ kind, href }]`. Unknown kinds (no icon) are skipped.
10
+ *
11
+ * @param {{ socials?: {kind: string, href: string}[],
12
+ * justify?: string, className?: string }} props
13
+ */
14
+
15
+ /* Inline SVGs, fill: currentColor so they inherit the themed text color. */
16
+ function GithubIcon() {
17
+ return (
18
+ <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
19
+ <path
20
+ fillRule="evenodd"
21
+ clipRule="evenodd"
22
+ d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z"
23
+ />
24
+ </svg>
25
+ );
26
+ }
27
+ function XIcon() {
28
+ return (
29
+ <svg viewBox="0 0 1200 1227" fill="currentColor" aria-hidden="true">
30
+ <path d="M714.163 519.284 1160.89 0h-105.86L667.137 450.887 357.328 0H0l468.492 681.821L0 1226.37h105.866l409.625-476.152 327.181 476.152H1200L714.137 519.284h.026ZM569.165 687.828l-47.468-67.894-377.686-540.24h162.604l304.797 435.991 47.468 67.894 396.2 566.721H892.476L569.165 687.854v-.026Z" />
31
+ </svg>
32
+ );
33
+ }
34
+ function YoutubeIcon() {
35
+ return (
36
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
37
+ <path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
38
+ </svg>
39
+ );
40
+ }
41
+ function LinkedinIcon() {
42
+ return (
43
+ <svg viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">
44
+ <path d="M218.123 218.127h-37.931v-59.403c0-14.165-.253-32.4-19.728-32.4-19.756 0-22.779 15.434-22.779 31.369v60.43h-37.93V95.967h36.413v16.694h.51a39.907 39.907 0 0 1 35.928-19.733c38.445 0 45.533 25.288 45.533 58.186l-.016 67.013ZM56.955 79.27c-12.157.002-22.014-9.852-22.016-22.009-.002-12.157 9.851-22.014 22.008-22.016 12.157-.003 22.014 9.851 22.016 22.008A22.013 22.013 0 0 1 56.955 79.27m18.966 138.858H37.95V95.967h37.97v122.16ZM237.033.018H18.89C8.58-.098.125 8.161-.001 18.471v219.053c.122 10.315 8.576 18.582 18.89 18.474h218.144c10.336.128 18.823-8.139 18.966-18.474V18.454c-.147-10.33-8.635-18.588-18.966-18.453" />
45
+ </svg>
46
+ );
47
+ }
48
+
49
+ export const SOCIAL_ICONS = {
50
+ github: GithubIcon,
51
+ x: XIcon,
52
+ youtube: YoutubeIcon,
53
+ linkedin: LinkedinIcon,
54
+ };
55
+ const SOCIAL_LABELS = {
56
+ github: 'GitHub',
57
+ x: 'X',
58
+ youtube: 'YouTube',
59
+ linkedin: 'LinkedIn',
60
+ };
61
+
62
+ export default function SocialLinks({ socials = [], justify, className = '' }) {
63
+ const known = socials.filter((s) => s && SOCIAL_ICONS[s.kind]);
64
+ if (!known.length) return null;
65
+ return (
66
+ <Cluster
67
+ space="var(--s-2)"
68
+ align="center"
69
+ justify={justify}
70
+ className={`velu-socials ${className}`.trim()}
71
+ >
72
+ {known.map((s, i) => {
73
+ const Icon = SOCIAL_ICONS[s.kind];
74
+ return (
75
+ <a
76
+ key={i}
77
+ href={s.href}
78
+ target="_blank"
79
+ rel="noreferrer"
80
+ aria-label={SOCIAL_LABELS[s.kind] ?? s.kind}
81
+ className="velu-social"
82
+ data-kind={s.kind}
83
+ >
84
+ <Icon />
85
+ </a>
86
+ );
87
+ })}
88
+ </Cluster>
89
+ );
90
+ }
@@ -1,65 +1,65 @@
1
- import React, { Children, cloneElement, isValidElement } from 'react';
2
-
3
- /**
4
- * Steps + Step — numbered procedural steps with a continuous accent rail
5
- * down the left edge.
6
- *
7
- * <Steps>
8
- * <Step title="First Step">These are instructions…</Step>
9
- * <Step title="Second Step">…</Step>
10
- * <Step title="Third Step">…</Step>
11
- * </Steps>
12
- *
13
- * Steps assigns 1-based `index` to each <Step> child via cloneElement —
14
- * callers don't have to number them by hand. A <Step> rendered outside
15
- * <Steps> falls back to whatever `index` it was given (or 1).
16
- *
17
- * Each Step is a 2-column grid:
18
- *
19
- * [circle + line] title
20
- * body
21
- *
22
- * The marker column spans both rows. The vertical rail (line below the
23
- * circle) is `flex: 1`, so it stretches to fill the Step's full height —
24
- * meaning adjacent steps' rails meet end-to-end with no manual height
25
- * math. The last step keeps its rail too (matches the design).
26
- */
27
-
28
- export function Step({
29
- title,
30
- index = 1,
31
- children,
32
- className = '',
33
- ...rest
34
- }) {
35
- return (
36
- <div className={`velu-step ${className}`.trim()} {...rest}>
37
- <div className="velu-step__circle" aria-hidden="true">
38
- {index}
39
- </div>
40
- <div className="velu-step__line" aria-hidden="true" />
41
- {title != null && (
42
- <div className="velu-step__title">{title}</div>
43
- )}
44
- {children != null && children !== false && (
45
- <div className="velu-step__body">{children}</div>
46
- )}
47
- </div>
48
- );
49
- }
50
- Step.displayName = 'Step';
51
-
52
- export default function Steps({ children, className = '', ...rest }) {
53
- let i = 0;
54
- const numbered = Children.map(children, (child) => {
55
- if (!isValidElement(child)) return child;
56
- if (child.type !== Step && child.type?.displayName !== 'Step') return child;
57
- i += 1;
58
- return cloneElement(child, { index: child.props.index ?? i });
59
- });
60
- return (
61
- <div className={`velu-steps ${className}`.trim()} {...rest}>
62
- {numbered}
63
- </div>
64
- );
65
- }
1
+ import React, { Children, cloneElement, isValidElement } from 'react';
2
+
3
+ /**
4
+ * Steps + Step — numbered procedural steps with a continuous accent rail
5
+ * down the left edge.
6
+ *
7
+ * <Steps>
8
+ * <Step title="First Step">These are instructions…</Step>
9
+ * <Step title="Second Step">…</Step>
10
+ * <Step title="Third Step">…</Step>
11
+ * </Steps>
12
+ *
13
+ * Steps assigns 1-based `index` to each <Step> child via cloneElement —
14
+ * callers don't have to number them by hand. A <Step> rendered outside
15
+ * <Steps> falls back to whatever `index` it was given (or 1).
16
+ *
17
+ * Each Step is a 2-column grid:
18
+ *
19
+ * [circle + line] title
20
+ * body
21
+ *
22
+ * The marker column spans both rows. The vertical rail (line below the
23
+ * circle) is `flex: 1`, so it stretches to fill the Step's full height —
24
+ * meaning adjacent steps' rails meet end-to-end with no manual height
25
+ * math. The last step keeps its rail too (matches the design).
26
+ */
27
+
28
+ export function Step({
29
+ title,
30
+ index = 1,
31
+ children,
32
+ className = '',
33
+ ...rest
34
+ }) {
35
+ return (
36
+ <div className={`velu-step ${className}`.trim()} {...rest}>
37
+ <div className="velu-step__circle" aria-hidden="true">
38
+ {index}
39
+ </div>
40
+ <div className="velu-step__line" aria-hidden="true" />
41
+ {title != null && (
42
+ <div className="velu-step__title">{title}</div>
43
+ )}
44
+ {children != null && children !== false && (
45
+ <div className="velu-step__body">{children}</div>
46
+ )}
47
+ </div>
48
+ );
49
+ }
50
+ Step.displayName = 'Step';
51
+
52
+ export default function Steps({ children, className = '', ...rest }) {
53
+ let i = 0;
54
+ const numbered = Children.map(children, (child) => {
55
+ if (!isValidElement(child)) return child;
56
+ if (child.type !== Step && child.type?.displayName !== 'Step') return child;
57
+ i += 1;
58
+ return cloneElement(child, { index: child.props.index ?? i });
59
+ });
60
+ return (
61
+ <div className={`velu-steps ${className}`.trim()} {...rest}>
62
+ {numbered}
63
+ </div>
64
+ );
65
+ }
@@ -1,48 +1,48 @@
1
- import React from 'react';
2
- import { Sun, Moon } from 'lucide-react';
3
-
4
- /**
5
- * ThemeToggle — pill-shaped two-state light/dark switch. The thumb
6
- * sits at the inline-start in light mode and slides to the inline-end
7
- * in dark mode. A sun glyph rides the thumb in light, a moon in dark.
8
- *
9
- * SSR-safe by design: markup is identical regardless of theme (the
10
- * server doesn't know it), and the thumb position + which glyph shows
11
- * are decided purely by CSS via the `[data-theme]` attribute — NOT by
12
- * React state. So there is no hydration mismatch and no icon flash. JS
13
- * only handles the click: it flips `data-theme` and persists the
14
- * explicit choice (which then overrides the OS preference on future
15
- * visits, per the anti-flash script in the template).
16
- */
17
- export default function ThemeToggle({ className = '', ...rest }) {
18
- function toggle() {
19
- const root = document.documentElement;
20
- const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
21
- root.dataset.theme = next;
22
- try {
23
- localStorage.setItem('velu-theme', next);
24
- } catch {
25
- /* private mode / storage disabled — toggle still works for the session */
26
- }
27
- }
28
-
29
- return (
30
- <button
31
- type="button"
32
- onClick={toggle}
33
- aria-label="Toggle color theme"
34
- title="Toggle color theme"
35
- className={`velu-theme-toggle ${className}`.trim()}
36
- {...rest}
37
- >
38
- <Sun
39
- className="velu-theme-toggle__icon velu-theme-toggle__icon--sun"
40
- aria-hidden="true"
41
- />
42
- <Moon
43
- className="velu-theme-toggle__icon velu-theme-toggle__icon--moon"
44
- aria-hidden="true"
45
- />
46
- </button>
47
- );
48
- }
1
+ import React from 'react';
2
+ import { Sun, Moon } from 'lucide-react';
3
+
4
+ /**
5
+ * ThemeToggle — pill-shaped two-state light/dark switch. The thumb
6
+ * sits at the inline-start in light mode and slides to the inline-end
7
+ * in dark mode. A sun glyph rides the thumb in light, a moon in dark.
8
+ *
9
+ * SSR-safe by design: markup is identical regardless of theme (the
10
+ * server doesn't know it), and the thumb position + which glyph shows
11
+ * are decided purely by CSS via the `[data-theme]` attribute — NOT by
12
+ * React state. So there is no hydration mismatch and no icon flash. JS
13
+ * only handles the click: it flips `data-theme` and persists the
14
+ * explicit choice (which then overrides the OS preference on future
15
+ * visits, per the anti-flash script in the template).
16
+ */
17
+ export default function ThemeToggle({ className = '', ...rest }) {
18
+ function toggle() {
19
+ const root = document.documentElement;
20
+ const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
21
+ root.dataset.theme = next;
22
+ try {
23
+ localStorage.setItem('velu-theme', next);
24
+ } catch {
25
+ /* private mode / storage disabled — toggle still works for the session */
26
+ }
27
+ }
28
+
29
+ return (
30
+ <button
31
+ type="button"
32
+ onClick={toggle}
33
+ aria-label="Toggle color theme"
34
+ title="Toggle color theme"
35
+ className={`velu-theme-toggle ${className}`.trim()}
36
+ {...rest}
37
+ >
38
+ <Sun
39
+ className="velu-theme-toggle__icon velu-theme-toggle__icon--sun"
40
+ aria-hidden="true"
41
+ />
42
+ <Moon
43
+ className="velu-theme-toggle__icon velu-theme-toggle__icon--moon"
44
+ aria-hidden="true"
45
+ />
46
+ </button>
47
+ );
48
+ }