@rimelight/ui 0.0.24 → 0.0.26

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 (65) hide show
  1. package/package.json +7 -3
  2. package/src/components/Head.astro +199 -156
  3. package/src/components/ThemeToggle.astro +2 -2
  4. package/src/components/astro/RLAAccordion.astro +22 -0
  5. package/src/components/astro/RLAButton.astro +104 -118
  6. package/src/components/astro/RLAFooter.astro +17 -43
  7. package/src/components/astro/RLAHeader.astro +21 -80
  8. package/src/components/astro/RLAIcon.astro +14 -39
  9. package/src/components/astro/RLALogo.astro +14 -48
  10. package/src/components/astro/RLAPlaceholder.astro +11 -35
  11. package/src/components/astro/RLAQuote.astro +15 -0
  12. package/src/components/astro/RLAScrollToTop.astro +171 -221
  13. package/src/components/astro/RLASection.astro +210 -0
  14. package/src/components/vue/RLVButton.vue +166 -139
  15. package/src/components/vue/RLVFooter.vue +17 -54
  16. package/src/components/vue/RLVHeader.vue +22 -105
  17. package/src/components/vue/RLVIcon.vue +14 -45
  18. package/src/components/vue/RLVLogo.vue +11 -58
  19. package/src/components/vue/RLVPlaceholder.vue +14 -35
  20. package/src/components/vue/RLVScrollToTop.vue +18 -89
  21. package/src/components/vue/RLVSection.vue +166 -0
  22. package/src/composables/index.ts +0 -1
  23. package/src/env.d.ts +5 -0
  24. package/src/integrations/ui.ts +2 -6
  25. package/src/plugins/index.ts +5 -2
  26. package/src/plugins/starlightAddons/index.ts +3 -64
  27. package/src/plugins/starlightDocsApi/components/ApiEmits.astro +46 -0
  28. package/src/plugins/starlightDocsApi/components/ApiProps.astro +47 -0
  29. package/src/plugins/starlightDocsApi/components/ApiSlots.astro +50 -0
  30. package/src/plugins/starlightDocsApi/components/ApiTheme.astro +27 -0
  31. package/src/plugins/starlightDocsApi/extract-vue.ts +286 -0
  32. package/src/plugins/starlightDocsApi/index.ts +265 -0
  33. package/src/plugins/starlightDocsApi/types.ts +40 -0
  34. package/src/plugins/starlightDocsApi/utils.ts +45 -0
  35. package/src/themes/button.theme.ts +29 -14
  36. package/src/themes/footer.theme.ts +4 -6
  37. package/src/themes/header.theme.ts +4 -28
  38. package/src/themes/icon.theme.ts +4 -2
  39. package/src/themes/index.ts +15 -20
  40. package/src/themes/logo.theme.ts +4 -2
  41. package/src/themes/placeholder.theme.ts +4 -6
  42. package/src/themes/scroll-to-top.theme.ts +4 -6
  43. package/src/themes/section.theme.ts +187 -0
  44. package/src/types/componentVariant.ts +14 -0
  45. package/src/types/index.ts +1 -0
  46. package/src/utils/defineTheme.ts +11 -0
  47. package/src/utils/index.ts +3 -0
  48. package/src/utils/resolveClasses.ts +21 -0
  49. package/src/utils/useUi.ts +19 -0
  50. package/src/composables/useUi.ts +0 -27
  51. package/src/plugins/starlightAddons/Sidebar.astro +0 -89
  52. package/src/plugins/starlightAddons/data.ts +0 -39
  53. package/src/plugins/starlightAddons/libs/config.ts +0 -107
  54. package/src/plugins/starlightAddons/libs/content.ts +0 -20
  55. package/src/plugins/starlightAddons/libs/i18n.ts +0 -51
  56. package/src/plugins/starlightAddons/libs/locals.ts +0 -12
  57. package/src/plugins/starlightAddons/libs/pathname.ts +0 -22
  58. package/src/plugins/starlightAddons/libs/plugin.ts +0 -9
  59. package/src/plugins/starlightAddons/libs/sidebar.ts +0 -183
  60. package/src/plugins/starlightAddons/libs/vite.ts +0 -46
  61. package/src/plugins/starlightAddons/locals.d.ts +0 -14
  62. package/src/plugins/starlightAddons/middleware.ts +0 -132
  63. package/src/plugins/starlightAddons/overrides/Sidebar.astro +0 -8
  64. package/src/plugins/starlightAddons/schema.ts +0 -13
  65. package/src/plugins/starlightAddons/virtual.d.ts +0 -13
@@ -0,0 +1,286 @@
1
+ import { readFileSync } from "node:fs"
2
+ import ts from "typescript"
3
+ import type { PropMeta, SlotMeta, EmitMeta } from "./types"
4
+
5
+ function extractScriptBlock(content: string): string | null {
6
+ const match =
7
+ content.match(/<script[^>]*lang=["']ts["'][^>]*>([\s\S]*?)<\/script>/) ||
8
+ content.match(/<script[^>]*>([\s\S]*?)<\/script>/)
9
+ return match?.[1] ?? null
10
+ }
11
+
12
+ function extractTemplateBlock(content: string): string {
13
+ const match = content.match(/<template[^>]*>([\s\S]*?)<\/template>/)
14
+ return match?.[1] ?? ""
15
+ }
16
+
17
+ function getJSDocDescription(node: ts.Node): string {
18
+ const jsDocs = ts.getJSDocCommentsAndTags(node)
19
+ if (jsDocs.length === 0) return ""
20
+
21
+ const comment = jsDocs[0]
22
+ if (comment && ts.isJSDoc(comment)) {
23
+ const text = comment.comment
24
+ if (typeof text === "string") return text.trim()
25
+ if (Array.isArray(text))
26
+ return text
27
+ .map((t) => (typeof t === "string" ? t : t.text))
28
+ .join(" ")
29
+ .trim()
30
+ }
31
+ return ""
32
+ }
33
+
34
+ function extractPropsFromInterface(node: ts.InterfaceDeclaration): PropMeta[] {
35
+ const props: PropMeta[] = []
36
+ for (const member of node.members) {
37
+ if (ts.isPropertySignature(member) && member.name && ts.isIdentifier(member.name)) {
38
+ const propName = member.name.text
39
+ if (propName === "class" || propName.startsWith("data-")) continue
40
+ const type = member.type ? member.type.getText().replace(/^[^:]+:\s*/, "") : "unknown"
41
+ const isOptional = member.questionToken !== undefined
42
+ const description = getJSDocDescription(member)
43
+ props.push({ name: propName, type, required: !isOptional, description })
44
+ }
45
+ }
46
+ return props
47
+ }
48
+
49
+ function extractPropsFromTypeLiteral(
50
+ node: ts.TypeLiteralNode,
51
+ defaults?: Record<string, string>
52
+ ): PropMeta[] {
53
+ const props: PropMeta[] = []
54
+ for (const member of node.members) {
55
+ if (ts.isPropertySignature(member) && member.name && ts.isIdentifier(member.name)) {
56
+ const propName = member.name.text
57
+ if (propName === "class" || propName.startsWith("data-")) continue
58
+ const type = member.type ? member.type.getText().replace(/^[^:]+:\s*/, "") : "unknown"
59
+ const isOptional = member.questionToken !== undefined
60
+ const defaultVal = defaults?.[propName]
61
+ const prop: PropMeta = { name: propName, type, required: !isOptional, description: "" }
62
+ if (defaultVal !== undefined) prop.default = defaultVal
63
+ props.push(prop)
64
+ }
65
+ }
66
+ return props
67
+ }
68
+
69
+ function getTypeRefName(typeArg: ts.TypeNode): string | null {
70
+ if (ts.isTypeReferenceNode(typeArg) && ts.isIdentifier(typeArg.typeName)) {
71
+ return typeArg.typeName.text
72
+ }
73
+ return null
74
+ }
75
+
76
+ export function extractVueMetaDirect(filePath: string): {
77
+ props: PropMeta[]
78
+ slots: SlotMeta[]
79
+ emits: EmitMeta[]
80
+ } {
81
+ const content = readFileSync(filePath, "utf-8")
82
+ const scriptContent = extractScriptBlock(content)
83
+
84
+ const props: PropMeta[] = []
85
+ const slots: SlotMeta[] = []
86
+ const emits: EmitMeta[] = []
87
+
88
+ if (scriptContent) {
89
+ const sourceFile = ts.createSourceFile(
90
+ "component.ts",
91
+ scriptContent,
92
+ ts.ScriptTarget.Latest,
93
+ true,
94
+ ts.ScriptKind.TS
95
+ )
96
+
97
+ const interfaces = new Map<string, ts.InterfaceDeclaration>()
98
+ const destructuredDefaults: Record<string, string> = {}
99
+
100
+ function collectDeclarations(node: ts.Node) {
101
+ if (ts.isInterfaceDeclaration(node)) {
102
+ interfaces.set(node.name.text, node)
103
+ }
104
+ if (ts.isVariableStatement(node)) {
105
+ for (const decl of node.declarationList.declarations) {
106
+ if (
107
+ decl.initializer &&
108
+ ts.isCallExpression(decl.initializer) &&
109
+ ts.isObjectBindingPattern(decl.name)
110
+ ) {
111
+ const expr = decl.initializer.expression
112
+ const fnName = ts.isIdentifier(expr) ? expr.text : ""
113
+ if (fnName === "defineProps") {
114
+ for (const element of decl.name.elements) {
115
+ if (
116
+ ts.isBindingElement(element) &&
117
+ ts.isIdentifier(element.name) &&
118
+ element.initializer
119
+ ) {
120
+ destructuredDefaults[element.name.text] = element.initializer.getText()
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ ts.forEachChild(node, collectDeclarations)
128
+ }
129
+ collectDeclarations(sourceFile)
130
+
131
+ function visit(node: ts.Node) {
132
+ if (ts.isCallExpression(node)) {
133
+ const expr = node.expression
134
+ const fnName = ts.isIdentifier(expr) ? expr.text : ""
135
+
136
+ if (fnName === "defineProps") {
137
+ if (node.typeArguments && node.typeArguments.length > 0) {
138
+ const typeArg = node.typeArguments[0]!
139
+ const refName = getTypeRefName(typeArg)
140
+ if (refName) {
141
+ const iface = interfaces.get(refName)
142
+ if (iface) {
143
+ const extracted = extractPropsFromInterface(iface)
144
+ for (const p of extracted) {
145
+ const d = destructuredDefaults[p.name]
146
+ if (d !== undefined) p.default = d
147
+ }
148
+ props.push(...extracted)
149
+ }
150
+ } else if (ts.isTypeLiteralNode(typeArg)) {
151
+ props.push(...extractPropsFromTypeLiteral(typeArg))
152
+ }
153
+ } else if (node.arguments.length > 0) {
154
+ const arg = node.arguments[0]!
155
+ if (ts.isTypeLiteralNode(arg)) {
156
+ props.push(...extractPropsFromTypeLiteral(arg))
157
+ }
158
+ }
159
+ }
160
+
161
+ if (fnName === "withDefaults" && node.arguments.length > 0) {
162
+ const callArg = node.arguments[0]!
163
+ if (ts.isCallExpression(callArg)) {
164
+ const innerFn = ts.isIdentifier(callArg.expression) ? callArg.expression.text : ""
165
+ if (innerFn === "defineProps") {
166
+ const defaultsObj = node.arguments[1]!
167
+ const defaults: Record<string, string> = {}
168
+ if (ts.isObjectLiteralExpression(defaultsObj)) {
169
+ for (const prop of defaultsObj.properties) {
170
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
171
+ defaults[prop.name.text] = prop.initializer.getText()
172
+ }
173
+ }
174
+ }
175
+
176
+ if (callArg.typeArguments && callArg.typeArguments.length > 0) {
177
+ const typeArg = callArg.typeArguments[0]!
178
+ const refName = getTypeRefName(typeArg)
179
+ if (refName) {
180
+ const iface = interfaces.get(refName)
181
+ if (iface) {
182
+ const baseProps = extractPropsFromInterface(iface)
183
+ for (const p of baseProps) {
184
+ const d = defaults[p.name]
185
+ if (d !== undefined) p.default = d
186
+ }
187
+ props.push(...baseProps)
188
+ }
189
+ } else if (ts.isTypeLiteralNode(typeArg)) {
190
+ props.push(...extractPropsFromTypeLiteral(typeArg, defaults))
191
+ }
192
+ } else if (callArg.arguments.length > 0) {
193
+ const arg = callArg.arguments[0]!
194
+ if (ts.isTypeLiteralNode(arg)) {
195
+ props.push(...extractPropsFromTypeLiteral(arg, defaults))
196
+ }
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ if (fnName === "defineSlots") {
203
+ const typeArg = node.typeArguments?.[0]
204
+ if (typeArg && ts.isTypeLiteralNode(typeArg)) {
205
+ for (const member of typeArg.members) {
206
+ if (ts.isMethodSignature(member) && member.name && ts.isIdentifier(member.name)) {
207
+ const slotName = member.name.text
208
+ const bindings: Record<string, string> = {}
209
+ if (member.parameters.length > 0) {
210
+ const param = member.parameters[0]
211
+ if (param?.type) {
212
+ const typeText = param.type.getText()
213
+ bindings["props"] = typeText
214
+ }
215
+ }
216
+ slots.push({ name: slotName, bindings, description: "" })
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ if (fnName === "defineEmits") {
223
+ const typeArg = node.typeArguments?.[0]
224
+ const runtimeArg = node.arguments?.[0]
225
+
226
+ if (typeArg && ts.isTypeLiteralNode(typeArg)) {
227
+ for (const member of typeArg.members) {
228
+ if (
229
+ ts.isPropertySignature(member) &&
230
+ member.name &&
231
+ ts.isIdentifier(member.name) &&
232
+ member.type
233
+ ) {
234
+ const emitName = member.name.text
235
+ let payloadType = "void"
236
+ if (ts.isFunctionTypeNode(member.type) && member.type.parameters.length > 0) {
237
+ const param = member.type.parameters[0]
238
+ if (param?.type) {
239
+ payloadType = param.type.getText()
240
+ }
241
+ }
242
+ emits.push({ name: emitName, type: payloadType, description: "" })
243
+ }
244
+ }
245
+ } else if (runtimeArg && ts.isTypeLiteralNode(runtimeArg)) {
246
+ for (const member of runtimeArg.members) {
247
+ if (
248
+ ts.isPropertySignature(member) &&
249
+ member.name &&
250
+ ts.isIdentifier(member.name) &&
251
+ member.type
252
+ ) {
253
+ const emitName = member.name.text
254
+ let payloadType = "void"
255
+ if (ts.isFunctionTypeNode(member.type) && member.type.parameters.length > 0) {
256
+ const param = member.type.parameters[0]
257
+ if (param?.type) {
258
+ payloadType = param.type.getText()
259
+ }
260
+ }
261
+ emits.push({ name: emitName, type: payloadType, description: "" })
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+ ts.forEachChild(node, visit)
268
+ }
269
+
270
+ visit(sourceFile)
271
+ }
272
+
273
+ const templateContent = extractTemplateBlock(content)
274
+ const slotMatches = templateContent.matchAll(/<slot(?:\s+name=["']([^"']+)["'])?/g)
275
+ for (const match of slotMatches) {
276
+ const slotName = match[1] ?? "default"
277
+ if (!slots.some((s) => s.name === slotName)) {
278
+ slots.push({ name: slotName, bindings: {}, description: "" })
279
+ }
280
+ }
281
+ if (!slots.length) {
282
+ slots.push({ name: "default", bindings: {}, description: "" })
283
+ }
284
+
285
+ return { props, slots, emits }
286
+ }
@@ -0,0 +1,265 @@
1
+ import type { AstroIntegration } from "astro"
2
+ import virtual from "vite-plugin-virtual"
3
+ import path from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+ import { readFileSync, existsSync, readdirSync } from "node:fs"
6
+ import ts from "typescript"
7
+ import type { ComponentMeta, PropMeta, SlotMeta, EmitMeta, ThemeMeta } from "./types"
8
+ import { extractVueMetaDirect } from "./extract-vue"
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
11
+ const uiRoot = path.resolve(__dirname, "..", "..", "..")
12
+
13
+ function pascalToKebab(name: string): string {
14
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
15
+ }
16
+
17
+ function extractAstroPropsFromInterface(sourceFile: ts.SourceFile): PropMeta[] {
18
+ const props: PropMeta[] = []
19
+
20
+ function visit(node: ts.Node) {
21
+ if (
22
+ (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) &&
23
+ node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
24
+ ) {
25
+ const name = node.name.text
26
+ if (name.endsWith("Props") || name === "Props") {
27
+ if (ts.isInterfaceDeclaration(node)) {
28
+ for (const member of node.members) {
29
+ if (ts.isPropertySignature(member) && member.name && ts.isIdentifier(member.name)) {
30
+ const propName = member.name.text
31
+ if (propName === "class" || propName.startsWith("data-")) continue
32
+ const type = member.type ? member.type.getText().replace(/^[^:]+:\s*/, "") : "unknown"
33
+ const isOptional = member.questionToken !== undefined
34
+ props.push({ name: propName, type, required: !isOptional, description: "" })
35
+ }
36
+ }
37
+ } else if (ts.isTypeAliasDeclaration(node) && ts.isTypeLiteralNode(node.type)) {
38
+ for (const member of node.type.members) {
39
+ if (ts.isPropertySignature(member) && member.name && ts.isIdentifier(member.name)) {
40
+ const propName = member.name.text
41
+ if (propName === "class" || propName.startsWith("data-")) continue
42
+ const type = member.type ? member.type.getText().replace(/^[^:]+:\s*/, "") : "unknown"
43
+ const isOptional = member.questionToken !== undefined
44
+ props.push({ name: propName, type, required: !isOptional, description: "" })
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
50
+ ts.forEachChild(node, visit)
51
+ }
52
+
53
+ visit(sourceFile)
54
+ return props
55
+ }
56
+
57
+ function extractAstroMeta(filePath: string): { props: PropMeta[]; slots: SlotMeta[] } {
58
+ const content = readFileSync(filePath, "utf-8")
59
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/)
60
+ if (!frontmatterMatch) return { props: [], slots: [] }
61
+
62
+ const frontmatter = frontmatterMatch[1]
63
+ const sourceFile = ts.createSourceFile(
64
+ "component.ts",
65
+ frontmatter ?? "",
66
+ ts.ScriptTarget.Latest,
67
+ true,
68
+ ts.ScriptKind.TS
69
+ )
70
+
71
+ const props = extractAstroPropsFromInterface(sourceFile)
72
+
73
+ const slots: SlotMeta[] = []
74
+ const slotMatches = content.matchAll(/<slot(?:\s+name=["']([^"']+)["'])?/g)
75
+ for (const match of slotMatches) {
76
+ slots.push({ name: match[1] ?? "default", bindings: {}, description: "" })
77
+ }
78
+ if (!slots.length) slots.push({ name: "default", bindings: {}, description: "" })
79
+
80
+ return { props, slots }
81
+ }
82
+
83
+ function safeParse(code: string): unknown {
84
+ try {
85
+ const normalized = code
86
+ .replace(/'/g, '"')
87
+ .replace(/,\s*\}/g, "}")
88
+ .replace(/,\s*\]/g, "]")
89
+ return JSON.parse(normalized)
90
+ } catch {
91
+ return {}
92
+ }
93
+ }
94
+
95
+ function isStringOrArray(value: unknown): value is string | string[] {
96
+ return typeof value === "string" || Array.isArray(value)
97
+ }
98
+
99
+ function isStringRecord(value: unknown): value is Record<string, string | string[]> {
100
+ if (typeof value !== "object" || value === null) return false
101
+ return Object.values(value).every(isStringOrArray)
102
+ }
103
+
104
+ function isVariantRecord(
105
+ value: unknown
106
+ ): value is Record<string, Record<string, Record<string, string | string[]>>> {
107
+ if (typeof value !== "object" || value === null) return false
108
+ return Object.values(value).every(
109
+ (v) => typeof v === "object" && v !== null && Object.values(v).every(isStringRecord)
110
+ )
111
+ }
112
+
113
+ function isCompoundArray(value: unknown): value is Record<string, unknown>[] {
114
+ return Array.isArray(value) && value.every((v) => typeof v === "object" && v !== null)
115
+ }
116
+
117
+ function isDefaultVariants(value: unknown): value is Record<string, unknown> {
118
+ return typeof value === "object" && value !== null
119
+ }
120
+
121
+ function extractThemeMeta(componentName: string, themesDir: string): ThemeMeta | null {
122
+ if (!existsSync(themesDir)) return null
123
+
124
+ const themeFiles = readdirSync(themesDir).filter((f) => f.endsWith(".theme.ts"))
125
+ let themePath: string | null = null
126
+ for (const file of themeFiles) {
127
+ const baseName = file.replace(".theme.ts", "")
128
+ if (baseName === componentName.toLowerCase()) {
129
+ themePath = path.join(themesDir, file)
130
+ break
131
+ }
132
+ }
133
+
134
+ if (!themePath || !existsSync(themePath)) return null
135
+
136
+ const content = readFileSync(themePath, "utf-8")
137
+ const slotsMatch = content.match(/slots:\s*\[([^\]]*)\]/)
138
+ const baseMatch = content.match(/base:\s*(\{[\s\S]*?\n[ \t]*\})/)
139
+ const variantsMatch = content.match(/variants:\s*(\{[\s\S]*?\n[ \t]*\})/)
140
+ const compoundMatch = content.match(/compoundVariants:\s*(\[[\s\S]*?\n[ \t]*\])/)
141
+ const defaultVariantsMatch = content.match(/defaultVariants:\s*(\{[\s\S]*?\n[ \t]*\})/)
142
+
143
+ const slots = slotsMatch
144
+ ? slotsMatch[1]!
145
+ .split(",")
146
+ .map((s) => s.trim().replace(/["']/g, ""))
147
+ .filter(Boolean)
148
+ : []
149
+
150
+ const parsedBase = baseMatch ? safeParse(baseMatch[1]!) : {}
151
+ const parsedVariants = variantsMatch ? safeParse(variantsMatch[1]!) : {}
152
+ const parsedCompound = compoundMatch ? safeParse(compoundMatch[1]!) : []
153
+ const parsedDefaults = defaultVariantsMatch ? safeParse(defaultVariantsMatch[1]!) : {}
154
+
155
+ return {
156
+ slots,
157
+ base: isStringRecord(parsedBase) ? parsedBase : {},
158
+ variants: isVariantRecord(parsedVariants) ? parsedVariants : {},
159
+ compoundVariants: isCompoundArray(parsedCompound) ? parsedCompound : [],
160
+ defaultVariants: isDefaultVariants(parsedDefaults) ? parsedDefaults : {}
161
+ }
162
+ }
163
+
164
+ function getComponentNameFromFilename(filename: string): string {
165
+ return filename.replace(/^(RLA|RLV)/, "").replace(/\.(astro|vue)$/, "")
166
+ }
167
+
168
+ function extractAllMeta(): Record<string, ComponentMeta> {
169
+ const componentsDir = path.join(uiRoot, "src", "components")
170
+ const themesDir = path.join(uiRoot, "src", "themes")
171
+
172
+ const vueDir = path.join(componentsDir, "vue")
173
+ const astroDir = path.join(componentsDir, "astro")
174
+
175
+ const vueFiles = existsSync(vueDir)
176
+ ? readdirSync(vueDir).filter((f: string) => f.endsWith(".vue"))
177
+ : []
178
+ const astroFiles = existsSync(astroDir)
179
+ ? readdirSync(astroDir).filter((f: string) => f.endsWith(".astro") && f !== "Head.astro")
180
+ : []
181
+
182
+ const vueComponents = new Map<string, string>()
183
+ const astroComponents = new Map<string, string>()
184
+
185
+ for (const f of vueFiles) vueComponents.set(getComponentNameFromFilename(f), f)
186
+ for (const f of astroFiles) astroComponents.set(getComponentNameFromFilename(f), f)
187
+
188
+ const allNames = new Set([...vueComponents.keys(), ...astroComponents.keys()])
189
+ const result: Record<string, ComponentMeta> = {}
190
+
191
+ for (const name of [...allNames].toSorted()) {
192
+ const vueFile = vueComponents.get(name)
193
+ const astroFile = astroComponents.get(name)
194
+
195
+ let props: PropMeta[] = []
196
+ let slots: SlotMeta[] = []
197
+ let emits: EmitMeta[] = []
198
+ const frameworks: ("astro" | "vue")[] = []
199
+
200
+ if (vueFile) {
201
+ frameworks.push("vue")
202
+ try {
203
+ const vueMeta = extractVueMetaDirect(path.join(vueDir, vueFile))
204
+ props = vueMeta.props
205
+ slots = vueMeta.slots
206
+ emits = vueMeta.emits
207
+ } catch (err) {
208
+ console.warn(`[starlightDocsApi] Failed to extract Vue meta for ${vueFile}:`, err)
209
+ }
210
+ }
211
+
212
+ if (astroFile) {
213
+ frameworks.push("astro")
214
+ if (!vueFile) {
215
+ try {
216
+ const astroMeta = extractAstroMeta(path.join(astroDir, astroFile))
217
+ props = astroMeta.props
218
+ slots = astroMeta.slots
219
+ } catch (err) {
220
+ console.warn(`[starlightDocsApi] Failed to extract Astro meta for ${astroFile}:`, err)
221
+ }
222
+ }
223
+ }
224
+
225
+ const theme = extractThemeMeta(name, themesDir)
226
+
227
+ result[pascalToKebab(name)] = {
228
+ name,
229
+ astroComponent: astroFile ?? null,
230
+ vueComponent: vueFile ?? null,
231
+ frameworks,
232
+ props,
233
+ slots,
234
+ emits,
235
+ theme
236
+ }
237
+ }
238
+
239
+ return result
240
+ }
241
+
242
+ export interface StarlightDocsApiConfig {}
243
+
244
+ export default function starlightDocsApi(_config?: StarlightDocsApiConfig): AstroIntegration {
245
+ return {
246
+ name: "rimelight-ui-starlight-docs-api",
247
+ hooks: {
248
+ "astro:config:setup": ({ updateConfig, logger }) => {
249
+ logger.info("Initializing @rimelight/ui docs API")
250
+
251
+ const meta = extractAllMeta()
252
+
253
+ updateConfig({
254
+ vite: {
255
+ plugins: [
256
+ virtual({
257
+ "virtual:rimelight-ui-docs-meta": `export default ${JSON.stringify(meta)}`
258
+ })
259
+ ]
260
+ }
261
+ } as Record<string, unknown>)
262
+ }
263
+ }
264
+ }
265
+ }
@@ -0,0 +1,40 @@
1
+ export interface PropMeta {
2
+ name: string
3
+ type: string
4
+ default?: string
5
+ required: boolean
6
+ description: string
7
+ }
8
+
9
+ export interface SlotMeta {
10
+ name: string
11
+ bindings: Record<string, string>
12
+ description: string
13
+ }
14
+
15
+ export interface EmitMeta {
16
+ name: string
17
+ type: string
18
+ description: string
19
+ }
20
+
21
+ export interface ThemeMeta {
22
+ slots: string[]
23
+ base: Record<string, string | string[]>
24
+ variants: Record<string, Record<string, Record<string, string | string[]>>>
25
+ compoundVariants: Record<string, unknown>[]
26
+ defaultVariants: Record<string, unknown>
27
+ }
28
+
29
+ export interface ComponentMeta {
30
+ name: string
31
+ astroComponent: string | null
32
+ vueComponent: string | null
33
+ frameworks: ("astro" | "vue")[]
34
+ props: PropMeta[]
35
+ slots: SlotMeta[]
36
+ emits: EmitMeta[]
37
+ theme: ThemeMeta | null
38
+ }
39
+
40
+ export type ComponentMetaMap = Record<string, ComponentMeta>
@@ -0,0 +1,45 @@
1
+ import type { PropMeta, ThemeMeta } from "./types"
2
+
3
+ export function kebabToPascal(name: string): string {
4
+ return name
5
+ .split("-")
6
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
7
+ .join("")
8
+ }
9
+
10
+ export function filterProps(
11
+ props: PropMeta[],
12
+ options: { hide?: string[]; show?: string[] }
13
+ ): PropMeta[] {
14
+ if (options.show?.length) {
15
+ return props.filter((p) => options.show!.includes(p.name))
16
+ }
17
+ if (options.hide?.length) {
18
+ return props.filter((p) => !options.hide!.includes(p.name))
19
+ }
20
+ return props
21
+ }
22
+
23
+ export function generateThemeCode(componentName: string, theme: ThemeMeta): string {
24
+ const componentConfig: Record<string, unknown> = {
25
+ ...(theme.slots.length ? { slots: theme.slots } : {}),
26
+ ...(Object.keys(theme.base).length ? { base: theme.base } : {}),
27
+ ...(Object.keys(theme.variants).length ? { variants: theme.variants } : {}),
28
+ ...(theme.compoundVariants.length ? { compoundVariants: theme.compoundVariants } : {}),
29
+ ...(Object.keys(theme.defaultVariants).length ? { defaultVariants: theme.defaultVariants } : {})
30
+ }
31
+
32
+ return `// astro.config.ts
33
+ import { ui } from "@rimelight/ui"
34
+ import { defineConfig } from "astro/config"
35
+
36
+ export default defineConfig({
37
+ integrations: [
38
+ ui({
39
+ components: {
40
+ ${componentName}: ${JSON.stringify(componentConfig, null, 6).replace(/\n/g, "\n ")}
41
+ }
42
+ })
43
+ ]
44
+ })`
45
+ }