@nucel/ui 0.10.0 → 0.12.0
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/README.md +21 -1
- package/package.json +29 -5
- package/src/lib/components/ui/Alert.svelte +47 -0
- package/src/lib/components/ui/AppCard.svelte +76 -0
- package/src/lib/components/ui/BranchPill.svelte +19 -0
- package/src/lib/components/ui/CommentPill.svelte +12 -0
- package/src/lib/components/ui/KanbanBoard.svelte +27 -0
- package/src/lib/components/ui/KanbanCard.svelte +43 -0
- package/src/lib/components/ui/KanbanColumn.svelte +52 -0
- package/src/lib/components/ui/ListCard.svelte +9 -0
- package/src/lib/components/ui/PageHeader.svelte +25 -0
- package/src/lib/components/ui/PermissionChips.svelte +49 -0
- package/src/lib/components/ui/Section.svelte +21 -0
- package/src/lib/components/ui/SectionTitle.svelte +16 -0
- package/src/lib/components/ui/StatusPill.svelte +54 -0
- package/src/lib/components/ui/editor/RichEditor.svelte +580 -0
- package/src/lib/components/ui/editor/mention-suggestion.ts +144 -0
- package/src/lib/components/ui/table/Table.svelte +12 -0
- package/src/lib/components/ui/table/TableBody.svelte +10 -0
- package/src/lib/components/ui/table/TableCaption.svelte +10 -0
- package/src/lib/components/ui/table/TableCell.svelte +10 -0
- package/src/lib/components/ui/table/TableHead.svelte +10 -0
- package/src/lib/components/ui/table/TableHeader.svelte +10 -0
- package/src/lib/components/ui/table/TableRow.svelte +10 -0
- package/src/lib/components/ui/table/index.ts +7 -0
- package/src/lib/index.ts +50 -0
- package/src/lib/components/ColorInput.test.ts +0 -126
- package/src/lib/components/CopyButton.test.ts +0 -213
- package/src/lib/components/IconButton.test.ts +0 -139
- package/src/lib/utils/cn.test.ts +0 -993
package/README.md
CHANGED
|
@@ -217,9 +217,21 @@ bun run storybook
|
|
|
217
217
|
# Install dependencies
|
|
218
218
|
bun install
|
|
219
219
|
|
|
220
|
-
#
|
|
220
|
+
# Build = export-completeness guard + svelte-check (MUST pass before commit/publish)
|
|
221
|
+
bun run build
|
|
222
|
+
|
|
223
|
+
# Type-check only
|
|
221
224
|
bun run check
|
|
222
225
|
|
|
226
|
+
# Export-completeness guard standalone — fails if any component under
|
|
227
|
+
# src/lib/components is not reachable from the barrel (src/lib/index.ts).
|
|
228
|
+
# This prevents the regression class where a component silently drops out
|
|
229
|
+
# of the public surface and breaks the consuming app's build.
|
|
230
|
+
bun run check:exports
|
|
231
|
+
|
|
232
|
+
# Run the unit/component test suite (Vitest + Testing Library)
|
|
233
|
+
bun run test
|
|
234
|
+
|
|
223
235
|
# Run linter
|
|
224
236
|
bun run lint
|
|
225
237
|
|
|
@@ -230,6 +242,14 @@ bun run format
|
|
|
230
242
|
bun run build-storybook
|
|
231
243
|
```
|
|
232
244
|
|
|
245
|
+
### Adding a component
|
|
246
|
+
|
|
247
|
+
When you add a `.svelte` component under `src/lib/components`, you **must** also
|
|
248
|
+
export it from `src/lib/index.ts`. `bun run build` (and CI / `prepublishOnly`)
|
|
249
|
+
run `check:exports`, which fails the build if any component file is not
|
|
250
|
+
reachable from the barrel. If a file is intentionally internal, list it in the
|
|
251
|
+
`IGNORE` set in `scripts/check-exports.mjs`.
|
|
252
|
+
|
|
233
253
|
## License
|
|
234
254
|
|
|
235
255
|
MIT © Nucel Team
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nucel/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "A comprehensive Svelte 5 UI component library for Nucel projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"svelte": "./src/lib/index.ts",
|
|
@@ -13,12 +13,15 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"src/lib",
|
|
16
|
-
"src/styles.css"
|
|
16
|
+
"src/styles.css",
|
|
17
|
+
"!src/lib/**/*.test.ts",
|
|
18
|
+
"!src/lib/**/*.test.js"
|
|
17
19
|
],
|
|
18
20
|
"scripts": {
|
|
19
21
|
"dev": "vite",
|
|
20
|
-
"build": "svelte-check --tsconfig ./tsconfig.json",
|
|
22
|
+
"build": "node scripts/check-exports.mjs && svelte-check --tsconfig ./tsconfig.json",
|
|
21
23
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
24
|
+
"check:exports": "node scripts/check-exports.mjs",
|
|
22
25
|
"lint": "eslint .",
|
|
23
26
|
"format": "prettier --write .",
|
|
24
27
|
"format:check": "prettier --check .",
|
|
@@ -26,7 +29,7 @@
|
|
|
26
29
|
"test:watch": "vitest",
|
|
27
30
|
"storybook": "storybook dev -p 6006",
|
|
28
31
|
"build-storybook": "storybook build",
|
|
29
|
-
"prepublishOnly": "echo 'publishing @nucel/ui'"
|
|
32
|
+
"prepublishOnly": "node scripts/check-exports.mjs && echo 'publishing @nucel/ui'"
|
|
30
33
|
},
|
|
31
34
|
"repository": {
|
|
32
35
|
"type": "git",
|
|
@@ -46,6 +49,26 @@
|
|
|
46
49
|
"license": "MIT",
|
|
47
50
|
"dependencies": {
|
|
48
51
|
"@lucide/svelte": "^0.564.0",
|
|
52
|
+
"@tiptap/core": "^3.23.6",
|
|
53
|
+
"@tiptap/extension-character-count": "^3.23.6",
|
|
54
|
+
"@tiptap/extension-color": "^3.23.6",
|
|
55
|
+
"@tiptap/extension-highlight": "^3.23.6",
|
|
56
|
+
"@tiptap/extension-link": "^3.23.6",
|
|
57
|
+
"@tiptap/extension-mention": "^3.23.6",
|
|
58
|
+
"@tiptap/extension-placeholder": "^3.23.6",
|
|
59
|
+
"@tiptap/extension-subscript": "^3.23.6",
|
|
60
|
+
"@tiptap/extension-superscript": "^3.23.6",
|
|
61
|
+
"@tiptap/extension-table": "^3.23.6",
|
|
62
|
+
"@tiptap/extension-table-cell": "^3.23.6",
|
|
63
|
+
"@tiptap/extension-table-header": "^3.23.6",
|
|
64
|
+
"@tiptap/extension-table-row": "^3.23.6",
|
|
65
|
+
"@tiptap/extension-task-item": "^3.23.6",
|
|
66
|
+
"@tiptap/extension-task-list": "^3.23.6",
|
|
67
|
+
"@tiptap/extension-text-align": "^3.23.6",
|
|
68
|
+
"@tiptap/extension-text-style": "^3.23.6",
|
|
69
|
+
"@tiptap/extension-typography": "^3.23.6",
|
|
70
|
+
"@tiptap/extension-underline": "^3.23.6",
|
|
71
|
+
"@tiptap/starter-kit": "^3.23.6",
|
|
49
72
|
"bits-ui": "^2.16.1",
|
|
50
73
|
"clsx": "^2.1.1",
|
|
51
74
|
"dompurify": "^3.3.3",
|
|
@@ -54,7 +77,8 @@
|
|
|
54
77
|
"shiki": "^4.1.0",
|
|
55
78
|
"svelte-sonner": "^1.0.7",
|
|
56
79
|
"tailwind-merge": "^3.5.0",
|
|
57
|
-
"tailwind-variants": "^3.2.2"
|
|
80
|
+
"tailwind-variants": "^3.2.2",
|
|
81
|
+
"tippy.js": "^6.3.7"
|
|
58
82
|
},
|
|
59
83
|
"devDependencies": {
|
|
60
84
|
"@eslint/js": "^9.39.2",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Component, Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
variant = 'default',
|
|
7
|
+
title,
|
|
8
|
+
children,
|
|
9
|
+
icon: Icon,
|
|
10
|
+
class: className,
|
|
11
|
+
}: {
|
|
12
|
+
variant?: 'default' | 'destructive' | 'success' | 'warning' | 'info';
|
|
13
|
+
title?: string;
|
|
14
|
+
children?: Snippet;
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
icon?: any;
|
|
17
|
+
class?: string;
|
|
18
|
+
} = $props();
|
|
19
|
+
|
|
20
|
+
const variantClasses: Record<string, string> = {
|
|
21
|
+
default: 'border-border/60 bg-muted/40 text-foreground',
|
|
22
|
+
destructive: 'border-destructive/30 bg-destructive/10 text-destructive',
|
|
23
|
+
success: 'border-green-500/30 bg-green-500/10 text-green-600 dark:text-green-400',
|
|
24
|
+
warning: 'border-yellow-500/30 bg-yellow-500/10 text-yellow-500',
|
|
25
|
+
info: 'border-blue-500/30 bg-blue-500/10 text-blue-400',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const containerClass = $derived(
|
|
29
|
+
cn(
|
|
30
|
+
'flex items-start gap-3 rounded-md border px-4 py-3 text-sm',
|
|
31
|
+
variantClasses[variant],
|
|
32
|
+
className,
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<div class={containerClass} role="alert">
|
|
38
|
+
{#if Icon}
|
|
39
|
+
<Icon class="mt-0.5 h-4 w-4 shrink-0" />
|
|
40
|
+
{/if}
|
|
41
|
+
<div>
|
|
42
|
+
{#if title}
|
|
43
|
+
<p class="font-semibold">{title}</p>
|
|
44
|
+
{/if}
|
|
45
|
+
{@render children?.()}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Reusable card for a Nucel-App row. Used by:
|
|
3
|
+
// - Settings/Developer/Apps/Index (developer's owned apps)
|
|
4
|
+
// - Marketplace/Index (public apps)
|
|
5
|
+
// - Org/InstalledApps (apps installed on an org)
|
|
6
|
+
//
|
|
7
|
+
// The data model is intentionally flat — callers pass exactly what
|
|
8
|
+
// they want rendered and slot any actions (Install, Suspend, Uninstall,
|
|
9
|
+
// ChevronRight, etc.) into the trailing `actions` snippet.
|
|
10
|
+
//
|
|
11
|
+
// `meta` and `subtitle` are optional secondary lines so each call site
|
|
12
|
+
// can attach its own context (e.g. "by @owner-org · published 5d ago"
|
|
13
|
+
// for marketplace; "installed as deploy-bot[bot] · 3h ago" for org).
|
|
14
|
+
|
|
15
|
+
import { Bot } from '@lucide/svelte';
|
|
16
|
+
import PermissionChips from './PermissionChips.svelte';
|
|
17
|
+
import type { Snippet } from 'svelte';
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
name,
|
|
21
|
+
href,
|
|
22
|
+
icon_url = null,
|
|
23
|
+
subtitle = null,
|
|
24
|
+
meta = null,
|
|
25
|
+
permissions = null,
|
|
26
|
+
actions,
|
|
27
|
+
badges,
|
|
28
|
+
}: {
|
|
29
|
+
name: string;
|
|
30
|
+
href?: string;
|
|
31
|
+
icon_url?: string | null;
|
|
32
|
+
subtitle?: string | null;
|
|
33
|
+
meta?: Snippet | null;
|
|
34
|
+
permissions?: Record<string, string> | null;
|
|
35
|
+
actions?: Snippet;
|
|
36
|
+
badges?: Snippet;
|
|
37
|
+
} = $props();
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<div class="flex items-start gap-3 px-4 py-4">
|
|
41
|
+
<div class="flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-primary/10 text-primary">
|
|
42
|
+
{#if icon_url}
|
|
43
|
+
<img src={icon_url} alt="" class="h-full w-full object-cover" />
|
|
44
|
+
{:else}
|
|
45
|
+
<Bot class="h-4 w-4" />
|
|
46
|
+
{/if}
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="min-w-0 flex-1">
|
|
50
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
51
|
+
{#if href}
|
|
52
|
+
<a class="text-sm font-medium text-foreground hover:text-primary hover:underline" {href}>{name}</a>
|
|
53
|
+
{:else}
|
|
54
|
+
<span class="text-sm font-medium text-foreground">{name}</span>
|
|
55
|
+
{/if}
|
|
56
|
+
{#if badges}{@render badges()}{/if}
|
|
57
|
+
</div>
|
|
58
|
+
{#if subtitle}
|
|
59
|
+
<p class="mt-0.5 text-xs text-muted-foreground">{subtitle}</p>
|
|
60
|
+
{/if}
|
|
61
|
+
{#if meta}
|
|
62
|
+
<div class="mt-1 text-[11px] text-muted-foreground">{@render meta()}</div>
|
|
63
|
+
{/if}
|
|
64
|
+
{#if permissions}
|
|
65
|
+
<div class="mt-2">
|
|
66
|
+
<PermissionChips {permissions} compact />
|
|
67
|
+
</div>
|
|
68
|
+
{/if}
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{#if actions}
|
|
72
|
+
<div class="flex shrink-0 flex-col items-end gap-2">
|
|
73
|
+
{@render actions()}
|
|
74
|
+
</div>
|
|
75
|
+
{/if}
|
|
76
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { GitBranch } from '@lucide/svelte';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
name,
|
|
6
|
+
size = 'sm',
|
|
7
|
+
}: {
|
|
8
|
+
name: string;
|
|
9
|
+
size?: 'xs' | 'sm';
|
|
10
|
+
} = $props();
|
|
11
|
+
|
|
12
|
+
let sizeCls = $derived(size === 'xs' ? 'text-[10px] px-1.5 py-0.5' : 'text-[11px] px-2 py-0.5');
|
|
13
|
+
let iconSize = $derived(size === 'xs' ? 'h-2.5 w-2.5' : 'h-3 w-3');
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<span class="inline-flex items-center gap-1 rounded-md border border-border/60 bg-background font-mono text-foreground/80 {sizeCls}">
|
|
17
|
+
<GitBranch class={iconSize} />
|
|
18
|
+
{name}
|
|
19
|
+
</span>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { MessageCircle } from '@lucide/svelte';
|
|
3
|
+
|
|
4
|
+
let { count }: { count: number } = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
{#if count > 0}
|
|
8
|
+
<div class="flex shrink-0 items-center gap-1 rounded-md border border-border/60 px-2 py-1 text-xs text-muted-foreground">
|
|
9
|
+
<MessageCircle class="h-3 w-3" />
|
|
10
|
+
<span>{count}</span>
|
|
11
|
+
</div>
|
|
12
|
+
{/if}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Outer kanban container: a horizontally-scrollable row of columns plus
|
|
3
|
+
// an optional trailing slot for an "Add column" affordance.
|
|
4
|
+
//
|
|
5
|
+
// Composition: caller puts <KanbanColumn> elements (or anything else)
|
|
6
|
+
// into the default slot. DnD wiring stays out of this primitive — the
|
|
7
|
+
// consumer attaches dndzone to whichever container they want sortable.
|
|
8
|
+
|
|
9
|
+
import type { Snippet } from 'svelte';
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
children,
|
|
13
|
+
trailing,
|
|
14
|
+
}: {
|
|
15
|
+
children: Snippet;
|
|
16
|
+
trailing?: Snippet;
|
|
17
|
+
} = $props();
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<div class="flex items-start gap-3 overflow-x-auto pb-4">
|
|
21
|
+
{@render children()}
|
|
22
|
+
{#if trailing}
|
|
23
|
+
<div class="w-72 flex-shrink-0">
|
|
24
|
+
{@render trailing()}
|
|
25
|
+
</div>
|
|
26
|
+
{/if}
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Minimal card for a kanban column. Title is required; body is optional
|
|
3
|
+
// single-paragraph supplementary text. `onDelete` adds a hover-revealed
|
|
4
|
+
// X button. `extra` is a snippet for arbitrary chrome (badges, avatars,
|
|
5
|
+
// etc.) under the body.
|
|
6
|
+
|
|
7
|
+
import { X } from '@lucide/svelte';
|
|
8
|
+
import type { Snippet } from 'svelte';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
title,
|
|
12
|
+
body = null,
|
|
13
|
+
onDelete,
|
|
14
|
+
extra,
|
|
15
|
+
}: {
|
|
16
|
+
title: string;
|
|
17
|
+
body?: string | null;
|
|
18
|
+
onDelete?: () => void;
|
|
19
|
+
extra?: Snippet;
|
|
20
|
+
} = $props();
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<div class="group rounded-md border border-border bg-background p-2.5 text-sm shadow-sm">
|
|
24
|
+
<div class="flex items-start justify-between gap-1">
|
|
25
|
+
<span class="leading-snug">{title}</span>
|
|
26
|
+
{#if onDelete}
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
aria-label="Delete card"
|
|
30
|
+
class="opacity-0 transition-opacity group-hover:opacity-100"
|
|
31
|
+
onclick={onDelete}
|
|
32
|
+
>
|
|
33
|
+
<X class="h-3 w-3 text-muted-foreground hover:text-destructive" />
|
|
34
|
+
</button>
|
|
35
|
+
{/if}
|
|
36
|
+
</div>
|
|
37
|
+
{#if body}
|
|
38
|
+
<p class="mt-1 line-clamp-2 text-xs text-muted-foreground">{body}</p>
|
|
39
|
+
{/if}
|
|
40
|
+
{#if extra}
|
|
41
|
+
<div class="mt-1.5">{@render extra()}</div>
|
|
42
|
+
{/if}
|
|
43
|
+
</div>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// A single kanban column: header (title + count + optional delete),
|
|
3
|
+
// a body snippet for cards (where the caller attaches a dndzone), and
|
|
4
|
+
// a footer snippet for the per-column "Add card" affordance.
|
|
5
|
+
|
|
6
|
+
import { X } from '@lucide/svelte';
|
|
7
|
+
import type { Snippet } from 'svelte';
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
name,
|
|
11
|
+
count,
|
|
12
|
+
onDelete,
|
|
13
|
+
body,
|
|
14
|
+
footer,
|
|
15
|
+
}: {
|
|
16
|
+
name: string;
|
|
17
|
+
count: number;
|
|
18
|
+
/** When provided, renders a small delete-X in the header. */
|
|
19
|
+
onDelete?: () => void;
|
|
20
|
+
/** Sortable body — caller drops their dndzone-decorated container here. */
|
|
21
|
+
body: Snippet;
|
|
22
|
+
/** Optional footer (e.g. "+ Add card"). */
|
|
23
|
+
footer?: Snippet;
|
|
24
|
+
} = $props();
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<div class="w-72 flex-shrink-0 rounded-lg bg-muted/50 p-3">
|
|
28
|
+
<div class="mb-2 flex items-center justify-between">
|
|
29
|
+
<h3 class="text-sm font-semibold">{name}</h3>
|
|
30
|
+
<div class="flex items-center gap-1">
|
|
31
|
+
<span class="rounded-full bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
|
32
|
+
{count}
|
|
33
|
+
</span>
|
|
34
|
+
{#if onDelete}
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
aria-label={`Delete column ${name}`}
|
|
38
|
+
class="rounded p-0.5 text-muted-foreground hover:bg-background hover:text-destructive"
|
|
39
|
+
onclick={onDelete}
|
|
40
|
+
>
|
|
41
|
+
<X class="h-3 w-3" />
|
|
42
|
+
</button>
|
|
43
|
+
{/if}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{@render body()}
|
|
48
|
+
|
|
49
|
+
{#if footer}
|
|
50
|
+
{@render footer()}
|
|
51
|
+
{/if}
|
|
52
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
title,
|
|
6
|
+
subtitle,
|
|
7
|
+
actions,
|
|
8
|
+
}: {
|
|
9
|
+
title: string;
|
|
10
|
+
subtitle?: string;
|
|
11
|
+
actions?: Snippet;
|
|
12
|
+
} = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<div class="mb-5 flex items-start justify-between gap-4">
|
|
16
|
+
<div class="min-w-0">
|
|
17
|
+
<h1 class="text-lg font-semibold tracking-tight text-foreground">{title}</h1>
|
|
18
|
+
{#if subtitle}
|
|
19
|
+
<p class="mt-0.5 text-xs text-muted-foreground">{subtitle}</p>
|
|
20
|
+
{/if}
|
|
21
|
+
</div>
|
|
22
|
+
{#if actions}
|
|
23
|
+
<div class="flex shrink-0 items-center gap-2">{@render actions()}</div>
|
|
24
|
+
{/if}
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Compact permission chips for Nucel-App permission sets.
|
|
3
|
+
//
|
|
4
|
+
// Used by the marketplace card, installed-apps card, and the
|
|
5
|
+
// developer's per-app Show page. Filters out `none` automatically and
|
|
6
|
+
// sorts alphabetically so the visual order is stable across calls.
|
|
7
|
+
//
|
|
8
|
+
// `compact=true` (default) renders single-line `key:level` chips
|
|
9
|
+
// (used inside cards). `compact=false` renders a per-row list with
|
|
10
|
+
// primary-tinted level badges (used on the Show page).
|
|
11
|
+
|
|
12
|
+
let { permissions, compact = true }: {
|
|
13
|
+
permissions: Record<string, string> | null | undefined;
|
|
14
|
+
compact?: boolean;
|
|
15
|
+
} = $props();
|
|
16
|
+
|
|
17
|
+
const entries = $derived(
|
|
18
|
+
Object.entries(permissions ?? {})
|
|
19
|
+
.filter(([_, v]) => typeof v === 'string' && v !== 'none')
|
|
20
|
+
.sort(([a], [b]) => a.localeCompare(b)),
|
|
21
|
+
);
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
{#if entries.length === 0}
|
|
25
|
+
<span class="text-[11px] italic text-muted-foreground">No write access requested.</span>
|
|
26
|
+
{:else if compact}
|
|
27
|
+
<div class="flex flex-wrap gap-1">
|
|
28
|
+
{#each entries as [key, level]}
|
|
29
|
+
<span class="rounded-md border border-border/60 bg-muted/40 px-1.5 py-0.5 text-[11px]">
|
|
30
|
+
<code class="font-mono">{key}</code>:{level}
|
|
31
|
+
</span>
|
|
32
|
+
{/each}
|
|
33
|
+
</div>
|
|
34
|
+
{:else}
|
|
35
|
+
<ul class="space-y-1.5">
|
|
36
|
+
{#each entries as [key, level]}
|
|
37
|
+
<li class="flex items-center justify-between gap-3 rounded-md border border-border/60 bg-muted/30 px-3 py-1.5 text-xs">
|
|
38
|
+
<code class="font-mono">{key}</code>
|
|
39
|
+
<span
|
|
40
|
+
class="rounded-md border px-1.5 py-0.5 text-[11px] {level === 'write' || level === 'admin'
|
|
41
|
+
? 'border-primary/40 bg-primary/10 text-primary'
|
|
42
|
+
: 'border-border/60 bg-muted text-muted-foreground'}"
|
|
43
|
+
>
|
|
44
|
+
{level}
|
|
45
|
+
</span>
|
|
46
|
+
</li>
|
|
47
|
+
{/each}
|
|
48
|
+
</ul>
|
|
49
|
+
{/if}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
title,
|
|
7
|
+
children,
|
|
8
|
+
class: className,
|
|
9
|
+
}: {
|
|
10
|
+
title?: string;
|
|
11
|
+
children?: Snippet;
|
|
12
|
+
class?: string;
|
|
13
|
+
} = $props();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<div class={cn('rounded-xl border border-border bg-card p-5', className)}>
|
|
17
|
+
{#if title}
|
|
18
|
+
<h3 class="mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">{title}</h3>
|
|
19
|
+
{/if}
|
|
20
|
+
{@render children?.()}
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../../utils/cn.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
children,
|
|
7
|
+
class: className,
|
|
8
|
+
}: {
|
|
9
|
+
children: Snippet;
|
|
10
|
+
class?: string;
|
|
11
|
+
} = $props();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<h3 class={cn('mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground', className)}>
|
|
15
|
+
{@render children()}
|
|
16
|
+
</h3>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Check, X, Loader2, CircleSlash, Clock, AlertTriangle } from '@lucide/svelte';
|
|
3
|
+
|
|
4
|
+
type Status = 'success' | 'failure' | 'running' | 'pending' | 'cancelled' | 'warning';
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
status,
|
|
8
|
+
label,
|
|
9
|
+
size = 'sm',
|
|
10
|
+
}: {
|
|
11
|
+
status: Status;
|
|
12
|
+
label?: string;
|
|
13
|
+
size?: 'xs' | 'sm';
|
|
14
|
+
} = $props();
|
|
15
|
+
|
|
16
|
+
const defaults: Record<Status, string> = {
|
|
17
|
+
success: 'passed',
|
|
18
|
+
failure: 'failed',
|
|
19
|
+
running: 'running',
|
|
20
|
+
pending: 'pending',
|
|
21
|
+
cancelled: 'cancelled',
|
|
22
|
+
warning: 'warning',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const styles: Record<Status, string> = {
|
|
26
|
+
success: 'border-green-500/30 bg-green-500/10 text-green-500',
|
|
27
|
+
failure: 'border-destructive/30 bg-destructive/10 text-destructive',
|
|
28
|
+
running: 'border-blue-500/30 bg-blue-500/10 text-blue-400',
|
|
29
|
+
pending: 'border-yellow-500/30 bg-yellow-500/10 text-yellow-500',
|
|
30
|
+
cancelled: 'border-border/60 bg-muted text-muted-foreground',
|
|
31
|
+
warning: 'border-amber-500/30 bg-amber-500/10 text-amber-500',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let text = $derived(label ?? defaults[status]);
|
|
35
|
+
let sizeCls = $derived(size === 'xs' ? 'text-[10px] px-1.5 py-0.5' : 'text-[11px] px-2 py-0.5');
|
|
36
|
+
let iconSize = $derived(size === 'xs' ? 'h-2.5 w-2.5' : 'h-3 w-3');
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<span class="inline-flex items-center gap-1 rounded-full border font-medium {sizeCls} {styles[status]}">
|
|
40
|
+
{#if status === 'success'}
|
|
41
|
+
<Check class={iconSize} />
|
|
42
|
+
{:else if status === 'failure'}
|
|
43
|
+
<X class={iconSize} />
|
|
44
|
+
{:else if status === 'running'}
|
|
45
|
+
<Loader2 class="{iconSize} animate-spin" />
|
|
46
|
+
{:else if status === 'cancelled'}
|
|
47
|
+
<CircleSlash class={iconSize} />
|
|
48
|
+
{:else if status === 'warning'}
|
|
49
|
+
<AlertTriangle class={iconSize} />
|
|
50
|
+
{:else}
|
|
51
|
+
<Clock class={iconSize} />
|
|
52
|
+
{/if}
|
|
53
|
+
{text}
|
|
54
|
+
</span>
|