@veluai/velu 0.1.13 → 0.1.14

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 (96) hide show
  1. package/README.md +80 -80
  2. package/dist/cli.js +18 -18
  3. package/package.json +3 -2
  4. package/runtime/velu-ui/components/Accordion.jsx +64 -64
  5. package/runtime/velu-ui/components/ApiClient.jsx +121 -121
  6. package/runtime/velu-ui/components/ApiField.jsx +87 -87
  7. package/runtime/velu-ui/components/ApiPath.jsx +63 -63
  8. package/runtime/velu-ui/components/ApiSidebar.jsx +122 -122
  9. package/runtime/velu-ui/components/AskBar.jsx +71 -71
  10. package/runtime/velu-ui/components/Callout.jsx +114 -114
  11. package/runtime/velu-ui/components/Card.jsx +131 -131
  12. package/runtime/velu-ui/components/Chatbot.jsx +596 -596
  13. package/runtime/velu-ui/components/CodeBlock.jsx +375 -375
  14. package/runtime/velu-ui/components/Columns.jsx +56 -56
  15. package/runtime/velu-ui/components/ErrorCard.jsx +138 -138
  16. package/runtime/velu-ui/components/Field.jsx +81 -81
  17. package/runtime/velu-ui/components/Image.jsx +163 -163
  18. package/runtime/velu-ui/components/MethodBadge.jsx +31 -31
  19. package/runtime/velu-ui/components/NavSelect.jsx +108 -108
  20. package/runtime/velu-ui/components/PageFeedback.jsx +219 -219
  21. package/runtime/velu-ui/components/PageFooter.jsx +213 -213
  22. package/runtime/velu-ui/components/PageHeader.jsx +414 -414
  23. package/runtime/velu-ui/components/PageNav.jsx +77 -77
  24. package/runtime/velu-ui/components/PoweredBy.jsx +51 -51
  25. package/runtime/velu-ui/components/Prompt.jsx +115 -115
  26. package/runtime/velu-ui/components/Search.jsx +411 -366
  27. package/runtime/velu-ui/components/Sidebar.jsx +191 -191
  28. package/runtime/velu-ui/components/Steps.jsx +65 -65
  29. package/runtime/velu-ui/components/ThemeToggle.jsx +48 -48
  30. package/runtime/velu-ui/components/Toc.jsx +537 -537
  31. package/runtime/velu-ui/components/TocBar.jsx +195 -195
  32. package/runtime/velu-ui/components/Tree.jsx +87 -87
  33. package/runtime/velu-ui/components/TryItBar.jsx +90 -90
  34. package/runtime/velu-ui/components/accordion.css +92 -92
  35. package/runtime/velu-ui/components/api.css +479 -479
  36. package/runtime/velu-ui/components/ask-bar.css +94 -94
  37. package/runtime/velu-ui/components/card.css +105 -105
  38. package/runtime/velu-ui/components/chatbot.css +617 -617
  39. package/runtime/velu-ui/components/code-block.css +263 -263
  40. package/runtime/velu-ui/components/docs-layout.css +784 -775
  41. package/runtime/velu-ui/components/field.css +82 -82
  42. package/runtime/velu-ui/components/image.css +237 -237
  43. package/runtime/velu-ui/components/nav-select.css +157 -157
  44. package/runtime/velu-ui/components/page-feedback.css +241 -241
  45. package/runtime/velu-ui/components/page-footer.css +130 -130
  46. package/runtime/velu-ui/components/page-header.css +520 -520
  47. package/runtime/velu-ui/components/page-nav.css +50 -50
  48. package/runtime/velu-ui/components/powered-by.css +66 -66
  49. package/runtime/velu-ui/components/prompt.css +99 -99
  50. package/runtime/velu-ui/components/search.css +307 -307
  51. package/runtime/velu-ui/components/sidebar.css +144 -144
  52. package/runtime/velu-ui/components/steps.css +77 -77
  53. package/runtime/velu-ui/components/theme-toggle.css +101 -101
  54. package/runtime/velu-ui/components/toc-bar.css +234 -234
  55. package/runtime/velu-ui/components/tree.css +49 -49
  56. package/runtime/velu-ui/index.js +46 -46
  57. package/runtime/velu-ui/lib/component-schemas.js +100 -100
  58. package/runtime/velu-ui/lib/copyText.js +64 -64
  59. package/runtime/velu-ui/lib/lang-icons.jsx +147 -147
  60. package/runtime/velu-ui/lib/prism-langs.js +957 -957
  61. package/runtime/velu-ui/lib/prism-loader.js +74 -74
  62. package/runtime/velu-ui/lib/resolveIcon.jsx +29 -29
  63. package/runtime/velu-ui/lib/scrollIntoNearestView.js +66 -66
  64. package/runtime/velu-ui/mdx-components.jsx +105 -105
  65. package/runtime/velu-ui/primitives/Cluster.jsx +49 -49
  66. package/runtime/velu-ui/primitives/Stack.jsx +63 -63
  67. package/runtime/velu-ui/primitives/Switcher.jsx +57 -57
  68. package/runtime/velu-ui/primitives/stack.css +3 -3
  69. package/runtime/velu-ui/primitives/switcher.css +25 -25
  70. package/runtime/velu-ui/styles.css +43 -43
  71. package/runtime/velu-ui/tokens.css +4 -4
  72. package/schema/velu.schema.json +167 -167
  73. package/src/lib/extract-mdx-error.js +170 -170
  74. package/src/lib/issues.js +159 -159
  75. package/src/lib/known-components.js +34 -34
  76. package/src/navigation.js +434 -434
  77. package/src/runtime/App.jsx +1490 -1476
  78. package/src/runtime/ErrorBoundary.jsx +54 -54
  79. package/src/runtime/client-entry.jsx +22 -22
  80. package/src/runtime/server-entry.jsx +16 -16
  81. package/src/template.html +48 -48
  82. package/templates/starter/ai-tools/claude-code.mdx +26 -26
  83. package/templates/starter/ai-tools/cursor.mdx +17 -17
  84. package/templates/starter/api-reference/endpoint/create.mdx +24 -24
  85. package/templates/starter/api-reference/endpoint/get.mdx +27 -27
  86. package/templates/starter/api-reference/introduction.mdx +28 -28
  87. package/templates/starter/development.mdx +19 -19
  88. package/templates/starter/essentials/code.mdx +29 -29
  89. package/templates/starter/essentials/images.mdx +29 -29
  90. package/templates/starter/essentials/markdown.mdx +25 -25
  91. package/templates/starter/essentials/navigation.mdx +39 -39
  92. package/templates/starter/essentials/settings.mdx +30 -30
  93. package/templates/starter/favicon.svg +6 -6
  94. package/templates/starter/index.mdx +31 -31
  95. package/templates/starter/quickstart.mdx +31 -31
  96. package/templates/starter/velu.json +33 -33
@@ -1,144 +1,144 @@
1
- /* Sidebar — sidebar-specific styling ONLY.
2
-
3
- Vertical rhythm (nav, section lists, nested sublists) is composed from the
4
- Stack primitive in Sidebar.jsx (DRY) — Stack supplies display/flex/gap
5
- inline. This file holds only what Stack cannot express: the indent rails,
6
- active accent rail, the horizontal item rows, summary marker, chevron
7
- rotation, and centralized icon sizing/stroke. All values are tokens. */
8
-
9
- .velu-sidebar {
10
- border-left: var(--border-width) solid var(--surface-color);
11
- margin-left: var(--s1);
12
- }
13
-
14
- .velu-sidebar__section {
15
- /* Sticky section header: each heading pins to the top of the scroll
16
- region until its own section scrolls past, then the next heading
17
- takes over (iOS-list style). It sticks within its section wrapper
18
- (Sidebar.jsx renders each section in its own Stack), which bounds
19
- the push-out. Full-width opaque background so scrolling items
20
- don't show behind it; --page-bg matches the surroundings, so the
21
- bar is invisible until it's actually covering scrolled content. */
22
- position: sticky;
23
- inset-block-start: 0;
24
- z-index: 1;
25
- background: var(--page-bg);
26
- display: flex;
27
- align-items: center;
28
- gap: var(--s-2);
29
- padding-block: var(--s-4);
30
- padding-inline: var(--s-3);
31
- /* A notch smaller than the default h5, with a little tracking so the
32
- uppercase label stays legible at the reduced size. */
33
- font-size: var(--f-h6);
34
- font-weight: 500;
35
- letter-spacing: 0.02em;
36
- text-transform: uppercase;
37
- }
38
- /* Fade zone just below the pinned heading: a gradient overlay
39
- (page-bg → transparent) that sits directly under the sticky header
40
- and scrolls with it. Items sliding up pass behind it and fade out
41
- before they tuck under the (crisp) heading. Absolutely positioned
42
- so it adds no resting layout space; pointer-events:none so it never
43
- eats clicks. Hidden by default — only shown once the nav is actually
44
- scrolled (the layout toggles it via data-fade-top on the scroll
45
- region), so a section's first item isn't faded at rest. */
46
- .velu-sidebar__section::after {
47
- content: '';
48
- position: absolute;
49
- inset-inline: 0;
50
- inset-block-start: 100%;
51
- block-size: var(--s3);
52
- background: linear-gradient(
53
- to bottom,
54
- var(--page-bg),
55
- color-mix(in srgb, var(--page-bg) 55%, transparent),
56
- transparent
57
- );
58
- pointer-events: none;
59
- opacity: 0;
60
- transition: opacity 0.15s ease;
61
- }
62
- /* When a heading becomes the newly-pinned one (data-stuck set by the
63
- runtime as you scroll past sections), fade + slide it in so the
64
- handoff from the previous heading reads as a transition rather than
65
- a hard swap. */
66
- .velu-sidebar__section[data-stuck='true'] {
67
- animation: velu-sidebar-stick-in 0.2s ease;
68
- }
69
- @keyframes velu-sidebar-stick-in {
70
- from {
71
- opacity: 0.3;
72
- transform: translateY(-0.2rem);
73
- }
74
- to {
75
- opacity: 1;
76
- transform: translateY(0);
77
- }
78
- }
79
-
80
- /* Lists are Stacks (flex/gap come from Stack inline) — only reset list chrome. */
81
- .velu-sidebar__list,
82
- .velu-sidebar__sublist {
83
- list-style: none;
84
- margin: 0;
85
- padding: 0;
86
- }
87
-
88
- .velu-sidebar__sublist {
89
- margin-left: var(--s0);
90
- border-left: var(--border-width) solid var(--border-color);
91
- }
92
-
93
- /* Item rows are horizontal (icon · label · chevron) — Stack is vertical-only,
94
- so these stay CSS until/if a Cluster/Inline primitive exists. */
95
- .velu-sidebar__item,
96
- .velu-sidebar__subitem {
97
- display: flex;
98
- align-items: center;
99
- gap: var(--s-3);
100
- padding-inline: var(--s0);
101
- color: var(--text-color);
102
- text-decoration: none;
103
- }
104
-
105
- .velu-sidebar__subitem {
106
- padding-block: var(--s-5);
107
- }
108
-
109
- .velu-sidebar__item--active {
110
- color: var(--accent-color);
111
- border-left: 2px solid var(--accent-color);
112
- }
113
-
114
- .velu-sidebar__summary {
115
- cursor: pointer;
116
- list-style: none;
117
- }
118
- .velu-sidebar__summary::-webkit-details-marker {
119
- display: none;
120
- }
121
-
122
- /* Icon sizing centralized here; stroke weight is the global --icon-stroke
123
- token (.lucide rule in base.css), not set per-component. */
124
- .velu-sidebar__icon {
125
- display: inline-flex;
126
- flex: none;
127
- inline-size: var(--f-body);
128
- block-size: var(--f-body);
129
- }
130
- .velu-sidebar__icon svg {
131
- inline-size: 100%;
132
- block-size: 100%;
133
- }
134
-
135
- .velu-sidebar__chevron {
136
- flex: none;
137
- inline-size: var(--f-body);
138
- block-size: var(--f-body);
139
- transition: transform 0.15s ease;
140
- }
141
-
142
- details[open] > .velu-sidebar__summary .velu-sidebar__chevron {
143
- transform: rotate(90deg);
144
- }
1
+ /* Sidebar — sidebar-specific styling ONLY.
2
+
3
+ Vertical rhythm (nav, section lists, nested sublists) is composed from the
4
+ Stack primitive in Sidebar.jsx (DRY) — Stack supplies display/flex/gap
5
+ inline. This file holds only what Stack cannot express: the indent rails,
6
+ active accent rail, the horizontal item rows, summary marker, chevron
7
+ rotation, and centralized icon sizing/stroke. All values are tokens. */
8
+
9
+ .velu-sidebar {
10
+ border-left: var(--border-width) solid var(--surface-color);
11
+ margin-left: var(--s1);
12
+ }
13
+
14
+ .velu-sidebar__section {
15
+ /* Sticky section header: each heading pins to the top of the scroll
16
+ region until its own section scrolls past, then the next heading
17
+ takes over (iOS-list style). It sticks within its section wrapper
18
+ (Sidebar.jsx renders each section in its own Stack), which bounds
19
+ the push-out. Full-width opaque background so scrolling items
20
+ don't show behind it; --page-bg matches the surroundings, so the
21
+ bar is invisible until it's actually covering scrolled content. */
22
+ position: sticky;
23
+ inset-block-start: 0;
24
+ z-index: 1;
25
+ background: var(--page-bg);
26
+ display: flex;
27
+ align-items: center;
28
+ gap: var(--s-2);
29
+ padding-block: var(--s-4);
30
+ padding-inline: var(--s-3);
31
+ /* A notch smaller than the default h5, with a little tracking so the
32
+ uppercase label stays legible at the reduced size. */
33
+ font-size: var(--f-h6);
34
+ font-weight: 500;
35
+ letter-spacing: 0.02em;
36
+ text-transform: uppercase;
37
+ }
38
+ /* Fade zone just below the pinned heading: a gradient overlay
39
+ (page-bg → transparent) that sits directly under the sticky header
40
+ and scrolls with it. Items sliding up pass behind it and fade out
41
+ before they tuck under the (crisp) heading. Absolutely positioned
42
+ so it adds no resting layout space; pointer-events:none so it never
43
+ eats clicks. Hidden by default — only shown once the nav is actually
44
+ scrolled (the layout toggles it via data-fade-top on the scroll
45
+ region), so a section's first item isn't faded at rest. */
46
+ .velu-sidebar__section::after {
47
+ content: '';
48
+ position: absolute;
49
+ inset-inline: 0;
50
+ inset-block-start: 100%;
51
+ block-size: var(--s3);
52
+ background: linear-gradient(
53
+ to bottom,
54
+ var(--page-bg),
55
+ color-mix(in srgb, var(--page-bg) 55%, transparent),
56
+ transparent
57
+ );
58
+ pointer-events: none;
59
+ opacity: 0;
60
+ transition: opacity 0.15s ease;
61
+ }
62
+ /* When a heading becomes the newly-pinned one (data-stuck set by the
63
+ runtime as you scroll past sections), fade + slide it in so the
64
+ handoff from the previous heading reads as a transition rather than
65
+ a hard swap. */
66
+ .velu-sidebar__section[data-stuck='true'] {
67
+ animation: velu-sidebar-stick-in 0.2s ease;
68
+ }
69
+ @keyframes velu-sidebar-stick-in {
70
+ from {
71
+ opacity: 0.3;
72
+ transform: translateY(-0.2rem);
73
+ }
74
+ to {
75
+ opacity: 1;
76
+ transform: translateY(0);
77
+ }
78
+ }
79
+
80
+ /* Lists are Stacks (flex/gap come from Stack inline) — only reset list chrome. */
81
+ .velu-sidebar__list,
82
+ .velu-sidebar__sublist {
83
+ list-style: none;
84
+ margin: 0;
85
+ padding: 0;
86
+ }
87
+
88
+ .velu-sidebar__sublist {
89
+ margin-left: var(--s0);
90
+ border-left: var(--border-width) solid var(--border-color);
91
+ }
92
+
93
+ /* Item rows are horizontal (icon · label · chevron) — Stack is vertical-only,
94
+ so these stay CSS until/if a Cluster/Inline primitive exists. */
95
+ .velu-sidebar__item,
96
+ .velu-sidebar__subitem {
97
+ display: flex;
98
+ align-items: center;
99
+ gap: var(--s-3);
100
+ padding-inline: var(--s0);
101
+ color: var(--text-color);
102
+ text-decoration: none;
103
+ }
104
+
105
+ .velu-sidebar__subitem {
106
+ padding-block: var(--s-5);
107
+ }
108
+
109
+ .velu-sidebar__item--active {
110
+ color: var(--accent-color);
111
+ border-left: 2px solid var(--accent-color);
112
+ }
113
+
114
+ .velu-sidebar__summary {
115
+ cursor: pointer;
116
+ list-style: none;
117
+ }
118
+ .velu-sidebar__summary::-webkit-details-marker {
119
+ display: none;
120
+ }
121
+
122
+ /* Icon sizing centralized here; stroke weight is the global --icon-stroke
123
+ token (.lucide rule in base.css), not set per-component. */
124
+ .velu-sidebar__icon {
125
+ display: inline-flex;
126
+ flex: none;
127
+ inline-size: var(--f-body);
128
+ block-size: var(--f-body);
129
+ }
130
+ .velu-sidebar__icon svg {
131
+ inline-size: 100%;
132
+ block-size: 100%;
133
+ }
134
+
135
+ .velu-sidebar__chevron {
136
+ flex: none;
137
+ inline-size: var(--f-body);
138
+ block-size: var(--f-body);
139
+ transition: transform 0.15s ease;
140
+ }
141
+
142
+ details[open] > .velu-sidebar__summary .velu-sidebar__chevron {
143
+ transform: rotate(90deg);
144
+ }
@@ -1,77 +1,77 @@
1
- /* Steps — numbered procedural list with a continuous accent rail
2
- running through every step's left margin. All values are tokens;
3
- light/dark via [data-theme]. */
4
-
5
- .velu-steps {
6
- display: block;
7
- }
8
-
9
- /* Each step is a 2-col / 2-row grid placed explicitly:
10
-
11
- col 1 col 2
12
- ┌──────────────────┬────────────────────┐
13
- │ circle (row 1, │ title (row 1, │
14
- │ align-self: end) │ align-self: end) │
15
- ├──────────────────┼────────────────────┤
16
- │ line (row 2, │ body (row 2) │
17
- │ 100% block-size) │ │
18
- └──────────────────┴────────────────────┘
19
-
20
- `align-self: end` on both row-1 items puts the circle's bottom and
21
- the title's bottom flush together — so an oversize circle no longer
22
- hovers above the title. Row 1 height is whichever child is taller
23
- (the circle). row-gap stays at 0 so the line touches the next step's
24
- circle with no visible break in the rail. */
25
- .velu-step {
26
- display: grid;
27
- grid-template-columns: var(--step-circle, 2em) 1fr;
28
- column-gap: var(--s0);
29
- row-gap: 0;
30
- }
31
-
32
- .velu-step__circle {
33
- grid-row: 1;
34
- grid-column: 1;
35
- align-self: center;
36
- justify-self: center;
37
- inline-size: var(--step-circle, 2em);
38
- block-size: var(--step-circle, 2em);
39
- border-radius: 50%;
40
- background: var(--accent-color);
41
- color: var(--page-bg);
42
- display: inline-flex;
43
- align-items: center;
44
- justify-content: center;
45
- font-size: var(--f-h6);
46
- line-height: 1;
47
- font-weight: var(--weight-medium);
48
- }
49
-
50
- .velu-step__line {
51
- grid-row: 2;
52
- grid-column: 1;
53
- justify-self: center;
54
- inline-size: 2px;
55
- block-size: 100%;
56
- min-block-size: var(--s2);
57
- background: var(--accent-color);
58
- }
59
-
60
- .velu-step__title {
61
- grid-row: 1;
62
- grid-column: 2;
63
- align-self: center;
64
- font-size: var(--f-h4);
65
- line-height: var(--lh-h4);
66
- font-weight: var(--weight-medium);
67
- color: var(--text-color);
68
- }
69
-
70
- .velu-step__body {
71
- grid-row: 2;
72
- grid-column: 2;
73
- padding-block: var(--s-1) var(--s2);
74
- color: var(--text-color);
75
- font-size: var(--f-body);
76
- line-height: var(--lh-body);
77
- }
1
+ /* Steps — numbered procedural list with a continuous accent rail
2
+ running through every step's left margin. All values are tokens;
3
+ light/dark via [data-theme]. */
4
+
5
+ .velu-steps {
6
+ display: block;
7
+ }
8
+
9
+ /* Each step is a 2-col / 2-row grid placed explicitly:
10
+
11
+ col 1 col 2
12
+ ┌──────────────────┬────────────────────┐
13
+ │ circle (row 1, │ title (row 1, │
14
+ │ align-self: end) │ align-self: end) │
15
+ ├──────────────────┼────────────────────┤
16
+ │ line (row 2, │ body (row 2) │
17
+ │ 100% block-size) │ │
18
+ └──────────────────┴────────────────────┘
19
+
20
+ `align-self: end` on both row-1 items puts the circle's bottom and
21
+ the title's bottom flush together — so an oversize circle no longer
22
+ hovers above the title. Row 1 height is whichever child is taller
23
+ (the circle). row-gap stays at 0 so the line touches the next step's
24
+ circle with no visible break in the rail. */
25
+ .velu-step {
26
+ display: grid;
27
+ grid-template-columns: var(--step-circle, 2em) 1fr;
28
+ column-gap: var(--s0);
29
+ row-gap: 0;
30
+ }
31
+
32
+ .velu-step__circle {
33
+ grid-row: 1;
34
+ grid-column: 1;
35
+ align-self: center;
36
+ justify-self: center;
37
+ inline-size: var(--step-circle, 2em);
38
+ block-size: var(--step-circle, 2em);
39
+ border-radius: 50%;
40
+ background: var(--accent-color);
41
+ color: var(--page-bg);
42
+ display: inline-flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ font-size: var(--f-h6);
46
+ line-height: 1;
47
+ font-weight: var(--weight-medium);
48
+ }
49
+
50
+ .velu-step__line {
51
+ grid-row: 2;
52
+ grid-column: 1;
53
+ justify-self: center;
54
+ inline-size: 2px;
55
+ block-size: 100%;
56
+ min-block-size: var(--s2);
57
+ background: var(--accent-color);
58
+ }
59
+
60
+ .velu-step__title {
61
+ grid-row: 1;
62
+ grid-column: 2;
63
+ align-self: center;
64
+ font-size: var(--f-h4);
65
+ line-height: var(--lh-h4);
66
+ font-weight: var(--weight-medium);
67
+ color: var(--text-color);
68
+ }
69
+
70
+ .velu-step__body {
71
+ grid-row: 2;
72
+ grid-column: 2;
73
+ padding-block: var(--s-1) var(--s2);
74
+ color: var(--text-color);
75
+ font-size: var(--f-body);
76
+ line-height: var(--lh-body);
77
+ }
@@ -1,101 +1,101 @@
1
- /* ThemeToggle — circular button with a sun/moon glyph; click flips
2
- the theme. Geometry is a 32 × 32 circle with an 18 × 18 glyph
3
- centred inside. The pill-with-slider design was replaced earlier;
4
- only the circle survives now.
5
-
6
- In light mode: --page-bg circle, sun glyph.
7
- In dark mode: --page-bg circle, moon glyph.
8
-
9
- Visibility model: BOTH icons are always rendered, stacked at the
10
- same `position: absolute` slot inside the button. Opacity controls
11
- which one is visible. We use opacity (not display: none) because:
12
-
13
- 1. Stacked icons render at the exact same point — no layout
14
- shift when the theme flips.
15
- 2. Opacity is not touched anywhere else for these elements, so
16
- the cascade is trivial — no risk of a stray rule (preflight,
17
- utility class, dev-time fallback) overriding `display: none`
18
- and revealing the wrong icon.
19
- 3. Identical markup on server + client → SSR-safe, no hydration
20
- mismatch and no icon flash.
21
-
22
- All values are tokens. */
23
-
24
- .velu-theme-toggle {
25
- /* `relative` so the absolutely-positioned icons inside are
26
- anchored to this button, not the page. */
27
- position: relative;
28
- display: inline-flex;
29
- align-items: center;
30
- justify-content: center;
31
- inline-size: 2rem;
32
- block-size: 2rem;
33
- border: var(--border-width) solid var(--border-color);
34
- border-radius: 999px;
35
- padding: 0;
36
- background: var(--page-bg);
37
- cursor: pointer;
38
- transition: border-color 0.12s ease, background 0.12s ease,
39
- transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
40
- }
41
- .velu-theme-toggle:hover {
42
- border-color: var(--accent-color);
43
- }
44
- /* Tactile press feedback on click. */
45
- .velu-theme-toggle:active {
46
- transform: scale(0.9);
47
- }
48
-
49
- /* Both icons stack here, centred. Each animates in/out on a theme flip:
50
- the hidden glyph is faded, shrunk, and rotated off its axis; the active
51
- one springs to full size at 0deg. Centring is done with margins (above),
52
- so `transform` is free to drive the rotate/scale animation. */
53
- .velu-theme-toggle__icon {
54
- position: absolute;
55
- inset-block-start: 50%;
56
- inset-inline-start: 50%;
57
- inline-size: 1.125rem;
58
- block-size: 1.125rem;
59
- margin-block-start: -0.5625rem; /* half of block-size */
60
- margin-inline-start: -0.5625rem; /* half of inline-size */
61
- color: var(--text-color);
62
- opacity: 0;
63
- transition: opacity 0.28s ease,
64
- transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
65
- pointer-events: none;
66
- }
67
-
68
- /* Hidden state per glyph — sun spins out clockwise, moon anti-clockwise,
69
- so the swap reads as one rotating through the other. */
70
- .velu-theme-toggle__icon--sun {
71
- transform: rotate(90deg) scale(0.4);
72
- }
73
- .velu-theme-toggle__icon--moon {
74
- transform: rotate(-90deg) scale(0.4);
75
- }
76
-
77
- /* Exactly one glyph is shown — the sun in light, the moon in dark — at full
78
- size, upright. Higher specificity than the hidden defaults above, so it
79
- wins for whichever glyph the active theme selects. */
80
- :root:not([data-theme='dark']) .velu-theme-toggle__icon--sun,
81
- [data-theme='dark'] .velu-theme-toggle__icon--moon {
82
- opacity: 1;
83
- transform: rotate(0deg) scale(1);
84
- }
85
-
86
- /* Honour reduced-motion: keep a plain crossfade — no spin, scale, or press. */
87
- @media (prefers-reduced-motion: reduce) {
88
- .velu-theme-toggle {
89
- transition: border-color 0.12s ease, background 0.12s ease;
90
- }
91
- .velu-theme-toggle:active {
92
- transform: none;
93
- }
94
- .velu-theme-toggle__icon {
95
- transition: opacity 0.2s ease;
96
- }
97
- .velu-theme-toggle__icon--sun,
98
- .velu-theme-toggle__icon--moon {
99
- transform: none;
100
- }
101
- }
1
+ /* ThemeToggle — circular button with a sun/moon glyph; click flips
2
+ the theme. Geometry is a 32 × 32 circle with an 18 × 18 glyph
3
+ centred inside. The pill-with-slider design was replaced earlier;
4
+ only the circle survives now.
5
+
6
+ In light mode: --page-bg circle, sun glyph.
7
+ In dark mode: --page-bg circle, moon glyph.
8
+
9
+ Visibility model: BOTH icons are always rendered, stacked at the
10
+ same `position: absolute` slot inside the button. Opacity controls
11
+ which one is visible. We use opacity (not display: none) because:
12
+
13
+ 1. Stacked icons render at the exact same point — no layout
14
+ shift when the theme flips.
15
+ 2. Opacity is not touched anywhere else for these elements, so
16
+ the cascade is trivial — no risk of a stray rule (preflight,
17
+ utility class, dev-time fallback) overriding `display: none`
18
+ and revealing the wrong icon.
19
+ 3. Identical markup on server + client → SSR-safe, no hydration
20
+ mismatch and no icon flash.
21
+
22
+ All values are tokens. */
23
+
24
+ .velu-theme-toggle {
25
+ /* `relative` so the absolutely-positioned icons inside are
26
+ anchored to this button, not the page. */
27
+ position: relative;
28
+ display: inline-flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ inline-size: 2rem;
32
+ block-size: 2rem;
33
+ border: var(--border-width) solid var(--border-color);
34
+ border-radius: 999px;
35
+ padding: 0;
36
+ background: var(--page-bg);
37
+ cursor: pointer;
38
+ transition: border-color 0.12s ease, background 0.12s ease,
39
+ transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1);
40
+ }
41
+ .velu-theme-toggle:hover {
42
+ border-color: var(--accent-color);
43
+ }
44
+ /* Tactile press feedback on click. */
45
+ .velu-theme-toggle:active {
46
+ transform: scale(0.9);
47
+ }
48
+
49
+ /* Both icons stack here, centred. Each animates in/out on a theme flip:
50
+ the hidden glyph is faded, shrunk, and rotated off its axis; the active
51
+ one springs to full size at 0deg. Centring is done with margins (above),
52
+ so `transform` is free to drive the rotate/scale animation. */
53
+ .velu-theme-toggle__icon {
54
+ position: absolute;
55
+ inset-block-start: 50%;
56
+ inset-inline-start: 50%;
57
+ inline-size: 1.125rem;
58
+ block-size: 1.125rem;
59
+ margin-block-start: -0.5625rem; /* half of block-size */
60
+ margin-inline-start: -0.5625rem; /* half of inline-size */
61
+ color: var(--text-color);
62
+ opacity: 0;
63
+ transition: opacity 0.28s ease,
64
+ transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
65
+ pointer-events: none;
66
+ }
67
+
68
+ /* Hidden state per glyph — sun spins out clockwise, moon anti-clockwise,
69
+ so the swap reads as one rotating through the other. */
70
+ .velu-theme-toggle__icon--sun {
71
+ transform: rotate(90deg) scale(0.4);
72
+ }
73
+ .velu-theme-toggle__icon--moon {
74
+ transform: rotate(-90deg) scale(0.4);
75
+ }
76
+
77
+ /* Exactly one glyph is shown — the sun in light, the moon in dark — at full
78
+ size, upright. Higher specificity than the hidden defaults above, so it
79
+ wins for whichever glyph the active theme selects. */
80
+ :root:not([data-theme='dark']) .velu-theme-toggle__icon--sun,
81
+ [data-theme='dark'] .velu-theme-toggle__icon--moon {
82
+ opacity: 1;
83
+ transform: rotate(0deg) scale(1);
84
+ }
85
+
86
+ /* Honour reduced-motion: keep a plain crossfade — no spin, scale, or press. */
87
+ @media (prefers-reduced-motion: reduce) {
88
+ .velu-theme-toggle {
89
+ transition: border-color 0.12s ease, background 0.12s ease;
90
+ }
91
+ .velu-theme-toggle:active {
92
+ transform: none;
93
+ }
94
+ .velu-theme-toggle__icon {
95
+ transition: opacity 0.2s ease;
96
+ }
97
+ .velu-theme-toggle__icon--sun,
98
+ .velu-theme-toggle__icon--moon {
99
+ transform: none;
100
+ }
101
+ }