mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-01 16:44:24 +00:00
Merge remote-tracking branch 'upstream/main' into add-gc-solid-registry
This commit is contained in:
5
.changeset/ten-gifts-check.md
Normal file
5
.changeset/ten-gifts-check.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
handling of apply directive inside utility
|
||||
@@ -472,7 +472,7 @@ Let's make the email column sortable.
|
||||
|
||||
### Update `<DataTable>`
|
||||
|
||||
```tsx showLineNumbers title="app/payments/data-table.tsx" showLineNumbers {3,6,10,18,25-28}
|
||||
```tsx showLineNumbers title="app/payments/data-table.tsx" showLineNumbers {3,6,10,18,25-29}
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
@@ -18,6 +18,7 @@ The `registry-item.json` schema is used to define your custom registry items.
|
||||
"https://example.com/r/foo"
|
||||
],
|
||||
"dependencies": ["is-even@3.0.0", "motion"],
|
||||
"devDependencies": ["tw-animate-css"],
|
||||
"files": [
|
||||
{
|
||||
"path": "registry/new-york/hello-world/hello-world.tsx",
|
||||
@@ -144,6 +145,21 @@ Use `@version` to specify the version of your registry item.
|
||||
}
|
||||
```
|
||||
|
||||
### devDependencies
|
||||
|
||||
The `devDependencies` property is used to specify the dev dependencies of your registry item. These are `npm` packages that are only needed during development.
|
||||
|
||||
Use `@version` to specify the version of the package.
|
||||
|
||||
```json title="registry-item.json" showLineNumbers
|
||||
{
|
||||
"devDependencies": [
|
||||
"tw-animate-css",
|
||||
"name@1.2.0"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### registryDependencies
|
||||
|
||||
Used for registry dependencies. Can be names, namespaced or URLs.
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
"url": "https://ui.8starlabs.com/r/{name}.json",
|
||||
"description": "A set of beautifully designed components designed for developers who want niche, high-utility UI elements that you won't find in standard libraries."
|
||||
},
|
||||
{
|
||||
"name": "@auth0",
|
||||
"homepage": "https://auth0.com",
|
||||
"url": "https://ui.auth0.com/r/{name}.json",
|
||||
"description": "Official Auth0 Universal Components for Web. Accelerate development with pre-built, embeddable UI for enterprise SSO, MFA, and organization management"
|
||||
},
|
||||
{
|
||||
"name": "@abui",
|
||||
"homepage": "https://abui.io",
|
||||
@@ -527,6 +533,12 @@
|
||||
"url": "https://shadcnui-blocks.com/r/{name}.json",
|
||||
"description": "A collection of premium, production-ready shadcn/ui blocks, components and templates."
|
||||
},
|
||||
{
|
||||
"name": "@shadcnuikit",
|
||||
"homepage": "https://shadcnuikit.com",
|
||||
"url": "https://shadcnuikit.com/r/{name}.json",
|
||||
"description": "Launch your projects faster with admin dashboards, website templates, components, blocks, and pre-built real-world examples."
|
||||
},
|
||||
{
|
||||
"name": "@shadcraft",
|
||||
"homepage": "https://free.shadcraft.com",
|
||||
@@ -783,6 +795,6 @@
|
||||
"name": "@pixelact-ui",
|
||||
"homepage": "https://pixelactui.com",
|
||||
"url": "https://pixelactui.com/r/{name}.json",
|
||||
"description": "Playful pixel-art style components library built on top of shadcn"
|
||||
"description": "Playful pixel art style components library built on top of shadcn. Perfect for retro style projects and games."
|
||||
}
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ export const RegistryErrorCode = {
|
||||
// Network errors
|
||||
NETWORK_ERROR: "NETWORK_ERROR",
|
||||
NOT_FOUND: "NOT_FOUND",
|
||||
GONE: "GONE",
|
||||
UNAUTHORIZED: "UNAUTHORIZED",
|
||||
FORBIDDEN: "FORBIDDEN",
|
||||
FETCH_ERROR: "FETCH_ERROR",
|
||||
@@ -90,6 +91,22 @@ export class RegistryNotFoundError extends RegistryError {
|
||||
}
|
||||
}
|
||||
|
||||
export class RegistryGoneError extends RegistryError {
|
||||
constructor(public readonly url: string, cause?: unknown) {
|
||||
const message = `The item at ${url} is no longer available. It may have been removed or expired.`
|
||||
|
||||
super(message, {
|
||||
code: RegistryErrorCode.GONE,
|
||||
statusCode: 410,
|
||||
cause,
|
||||
context: { url },
|
||||
suggestion:
|
||||
"This resource was previously available but has been permanently removed. Check if a newer version exists or contact the registry maintainer.",
|
||||
})
|
||||
this.name = "RegistryGoneError"
|
||||
}
|
||||
}
|
||||
|
||||
export class RegistryUnauthorizedError extends RegistryError {
|
||||
constructor(public readonly url: string, cause?: unknown) {
|
||||
const message = `You are not authorized to access the item at ${url}. If this is a remote registry, you may need to authenticate.`
|
||||
|
||||
@@ -2,6 +2,7 @@ import { REGISTRY_URL } from "@/src/registry/constants"
|
||||
import {
|
||||
RegistryFetchError,
|
||||
RegistryForbiddenError,
|
||||
RegistryGoneError,
|
||||
RegistryNotFoundError,
|
||||
RegistryUnauthorizedError,
|
||||
} from "@/src/registry/errors"
|
||||
@@ -30,6 +31,9 @@ const server = setupServer(
|
||||
http.get(`${REGISTRY_URL}/forbidden.json`, () => {
|
||||
return new HttpResponse(null, { status: 403 })
|
||||
}),
|
||||
http.get(`${REGISTRY_URL}/gone.json`, () => {
|
||||
return new HttpResponse(null, { status: 410 })
|
||||
}),
|
||||
http.get("https://external.com/component.json", () => {
|
||||
return HttpResponse.json({
|
||||
name: "external",
|
||||
@@ -123,6 +127,10 @@ describe("fetchRegistry", () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle 410 errors", async () => {
|
||||
await expect(fetchRegistry(["gone.json"])).rejects.toThrow(RegistryGoneError)
|
||||
})
|
||||
|
||||
it("should handle network errors", async () => {
|
||||
await expect(fetchRegistry(["error.json"])).rejects.toThrow()
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getRegistryHeadersFromContext } from "@/src/registry/context"
|
||||
import {
|
||||
RegistryFetchError,
|
||||
RegistryForbiddenError,
|
||||
RegistryGoneError,
|
||||
RegistryLocalFileError,
|
||||
RegistryNotFoundError,
|
||||
RegistryParseError,
|
||||
@@ -93,6 +94,10 @@ export async function fetchRegistry(
|
||||
throw new RegistryNotFoundError(url, messageFromServer)
|
||||
}
|
||||
|
||||
if (response.status === 410) {
|
||||
throw new RegistryGoneError(url, messageFromServer)
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
throw new RegistryForbiddenError(url, messageFromServer)
|
||||
}
|
||||
|
||||
@@ -317,7 +317,7 @@ function updateCssPlugin(css: z.infer<typeof registryItemCssSchema>) {
|
||||
postcss.comment({ text: "---break---" })
|
||||
)
|
||||
|
||||
// Add declarations with their values preserved
|
||||
// Add declarations with their values preserved.
|
||||
if (typeof properties === "object") {
|
||||
for (const [prop, value] of Object.entries(properties)) {
|
||||
if (typeof value === "string") {
|
||||
@@ -327,13 +327,38 @@ function updateCssPlugin(css: z.infer<typeof registryItemCssSchema>) {
|
||||
raws: { semicolon: true, before: "\n " },
|
||||
})
|
||||
atRule.append(decl)
|
||||
} else if (
|
||||
prop.startsWith("@") &&
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
Object.keys(value as Record<string, unknown>).length === 0
|
||||
) {
|
||||
// Handle at-rules with no body (e.g., @apply).
|
||||
const atRuleMatch = prop.match(/@([a-zA-Z-]+)\s*(.*)/)
|
||||
if (atRuleMatch) {
|
||||
const [, atRuleName, atRuleParams] = atRuleMatch
|
||||
const existingAtRule = atRule.nodes?.find(
|
||||
(node): node is AtRule =>
|
||||
node.type === "atrule" &&
|
||||
node.name === atRuleName &&
|
||||
node.params === atRuleParams
|
||||
)
|
||||
if (!existingAtRule) {
|
||||
const newAtRule = postcss.atRule({
|
||||
name: atRuleName,
|
||||
params: atRuleParams,
|
||||
raws: { semicolon: true, before: "\n " },
|
||||
})
|
||||
atRule.append(newAtRule)
|
||||
}
|
||||
}
|
||||
} else if (typeof value === "object") {
|
||||
processRule(atRule, prop, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Update existing utility class
|
||||
// Update existing utility class.
|
||||
if (typeof properties === "object") {
|
||||
for (const [prop, value] of Object.entries(properties)) {
|
||||
if (typeof value === "string") {
|
||||
@@ -351,6 +376,31 @@ function updateCssPlugin(css: z.infer<typeof registryItemCssSchema>) {
|
||||
existingDecl
|
||||
? existingDecl.replaceWith(decl)
|
||||
: utilityAtRule.append(decl)
|
||||
} else if (
|
||||
prop.startsWith("@") &&
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
Object.keys(value as Record<string, unknown>).length === 0
|
||||
) {
|
||||
// Handle at-rules with no body (e.g., @apply).
|
||||
const atRuleMatch = prop.match(/@([a-zA-Z-]+)\s*(.*)/)
|
||||
if (atRuleMatch) {
|
||||
const [, atRuleName, atRuleParams] = atRuleMatch
|
||||
const existingAtRule = utilityAtRule.nodes?.find(
|
||||
(node): node is AtRule =>
|
||||
node.type === "atrule" &&
|
||||
node.name === atRuleName &&
|
||||
node.params === atRuleParams
|
||||
)
|
||||
if (!existingAtRule) {
|
||||
const newAtRule = postcss.atRule({
|
||||
name: atRuleName,
|
||||
params: atRuleParams,
|
||||
raws: { semicolon: true, before: "\n " },
|
||||
})
|
||||
utilityAtRule.append(newAtRule)
|
||||
}
|
||||
}
|
||||
} else if (typeof value === "object") {
|
||||
processRule(utilityAtRule, prop, value)
|
||||
}
|
||||
|
||||
@@ -929,6 +929,114 @@ describe("transformCss", () => {
|
||||
`)
|
||||
})
|
||||
|
||||
test("should handle @apply inside @utility", async () => {
|
||||
const input = `@import "tailwindcss";`
|
||||
|
||||
const result = await transformCss(input, {
|
||||
"@utility custom-btn": {
|
||||
"@apply px-4 py-2 rounded-md": {},
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"@import "tailwindcss";
|
||||
|
||||
@utility custom-btn {
|
||||
@apply px-4 py-2 rounded-md;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test("should handle @apply mixed with declarations inside @utility", async () => {
|
||||
const input = `@import "tailwindcss";`
|
||||
|
||||
const result = await transformCss(input, {
|
||||
"@utility custom-card": {
|
||||
"@apply bg-white shadow-md": {},
|
||||
"border-radius": "0.5rem",
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"@import "tailwindcss";
|
||||
|
||||
@utility custom-card {
|
||||
@apply bg-white shadow-md;
|
||||
border-radius: 0.5rem;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test("should handle multiple @apply inside @utility", async () => {
|
||||
const input = `@import "tailwindcss";`
|
||||
|
||||
const result = await transformCss(input, {
|
||||
"@utility custom-input": {
|
||||
"@apply border border-gray-300 rounded-md": {},
|
||||
"@apply focus:ring-2 focus:ring-blue-500": {},
|
||||
padding: "0.5rem",
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"@import "tailwindcss";
|
||||
|
||||
@utility custom-input {
|
||||
@apply border border-gray-300 rounded-md;
|
||||
@apply focus:ring-2 focus:ring-blue-500;
|
||||
padding: 0.5rem;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test("should add @apply to existing @utility", async () => {
|
||||
const input = `@import "tailwindcss";
|
||||
|
||||
@utility custom-alert {
|
||||
font-size: 1rem;
|
||||
}`
|
||||
|
||||
const result = await transformCss(input, {
|
||||
"@utility custom-alert": {
|
||||
"@apply font-bold text-red-500": {},
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"@import "tailwindcss";
|
||||
|
||||
@utility custom-alert {
|
||||
font-size: 1rem;
|
||||
@apply font-bold text-red-500;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test("should not duplicate @apply inside existing @utility", async () => {
|
||||
const input = `@import "tailwindcss";
|
||||
|
||||
@utility custom-badge {
|
||||
@apply inline-flex items-center;
|
||||
font-size: 0.75rem;
|
||||
}`
|
||||
|
||||
const result = await transformCss(input, {
|
||||
"@utility custom-badge": {
|
||||
"@apply inline-flex items-center": {},
|
||||
"font-size": "0.875rem",
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"@import "tailwindcss";
|
||||
|
||||
@utility custom-badge {
|
||||
@apply inline-flex items-center;
|
||||
font-size: 0.875rem;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test("should replace existing keyframes instead of duplicating", async () => {
|
||||
const input = `@import "tailwindcss";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user