@indinno/digicode-ui 0.1.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/LICENSE +21 -0
- package/README.md +167 -0
- package/bin/digicode.mjs +136 -0
- package/package.json +31 -0
- package/registry/components/accordion.tsx +41 -0
- package/registry/components/alert.tsx +21 -0
- package/registry/components/avatar.tsx +18 -0
- package/registry/components/badge.tsx +9 -0
- package/registry/components/breadcrumb.tsx +22 -0
- package/registry/components/button.tsx +38 -0
- package/registry/components/card.tsx +10 -0
- package/registry/components/checkbox.tsx +15 -0
- package/registry/components/dialog.tsx +46 -0
- package/registry/components/drawer.tsx +42 -0
- package/registry/components/dropdown-menu.tsx +38 -0
- package/registry/components/empty-state.tsx +20 -0
- package/registry/components/input.tsx +28 -0
- package/registry/components/pagination.tsx +21 -0
- package/registry/components/progress.tsx +17 -0
- package/registry/components/radio-group.tsx +40 -0
- package/registry/components/select.tsx +33 -0
- package/registry/components/separator.tsx +10 -0
- package/registry/components/skeleton.tsx +6 -0
- package/registry/components/spinner.tsx +11 -0
- package/registry/components/stat-card.tsx +21 -0
- package/registry/components/switch.tsx +15 -0
- package/registry/components/table.tsx +11 -0
- package/registry/components/tabs.tsx +52 -0
- package/registry/components/textarea.tsx +22 -0
- package/registry/components/tooltip.tsx +16 -0
- package/registry/lib/cn.ts +3 -0
- package/registry/registry.json +32 -0
- package/registry/styles/digicode.css +253 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DigiCode
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# DigiCode
|
|
2
|
+
|
|
3
|
+
A source-owned React UI primitive library with a shadcn-style CLI. It contains **UI elements only** — no pages, marketing sections, headers, footers, dashboards, or application-specific content.
|
|
4
|
+
|
|
5
|
+
The visual language is based on the clean DigiCode UI: dark ink actions, blue accents, soft surfaces, 12–18px radii, restrained borders, and accessible focus states.
|
|
6
|
+
|
|
7
|
+
## Components
|
|
8
|
+
|
|
9
|
+
- Accordion
|
|
10
|
+
- Alert
|
|
11
|
+
- Avatar
|
|
12
|
+
- Badge
|
|
13
|
+
- Breadcrumb
|
|
14
|
+
- Button
|
|
15
|
+
- Card
|
|
16
|
+
- Checkbox
|
|
17
|
+
- Dialog
|
|
18
|
+
- Drawer
|
|
19
|
+
- Dropdown Menu
|
|
20
|
+
- Empty State
|
|
21
|
+
- Input
|
|
22
|
+
- Pagination
|
|
23
|
+
- Progress
|
|
24
|
+
- Radio Group
|
|
25
|
+
- Select
|
|
26
|
+
- Separator
|
|
27
|
+
- Skeleton
|
|
28
|
+
- Spinner
|
|
29
|
+
- Stat Card
|
|
30
|
+
- Switch
|
|
31
|
+
- Table
|
|
32
|
+
- Tabs
|
|
33
|
+
- Textarea
|
|
34
|
+
- Tooltip
|
|
35
|
+
|
|
36
|
+
## Publish to npm
|
|
37
|
+
|
|
38
|
+
1. Change the package name in `package.json` if needed. The starter uses:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
"name": "@indinno/digicode-ui"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
2. Login and publish:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm login
|
|
48
|
+
npm publish --access public
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Use it like shadcn
|
|
52
|
+
|
|
53
|
+
Initialize the target React/Next.js project:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx @indinno/digicode-ui init
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Add only the components you need:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx @indinno/digicode-ui add button input card badge
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or install the full primitive set:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx @indinno/digicode-ui add all
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
List available components:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx @indinno/digicode-ui list
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The CLI copies source into either:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
src/components/ui
|
|
81
|
+
src/lib/cn.ts
|
|
82
|
+
src/styles/digicode.css
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
or, for projects without a `src` directory:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
components/ui
|
|
89
|
+
lib/cn.ts
|
|
90
|
+
styles/digicode.css
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
That means the consuming project **owns the source code**, just like the shadcn model. You can edit a copied button or card locally without waiting for a library release.
|
|
94
|
+
|
|
95
|
+
## Import the stylesheet once
|
|
96
|
+
|
|
97
|
+
For a Next.js app using `src/app`, add this to the root layout or global stylesheet entry:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import "../styles/digicode.css";
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Then use copied components normally:
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { Button } from "@/components/ui/button";
|
|
107
|
+
import {
|
|
108
|
+
Card,
|
|
109
|
+
CardContent,
|
|
110
|
+
CardHeader,
|
|
111
|
+
CardTitle,
|
|
112
|
+
} from "@/components/ui/card";
|
|
113
|
+
import { Input } from "@/components/ui/input";
|
|
114
|
+
|
|
115
|
+
export function Example() {
|
|
116
|
+
return (
|
|
117
|
+
<Card elevated>
|
|
118
|
+
<CardHeader>
|
|
119
|
+
<CardTitle>Client details</CardTitle>
|
|
120
|
+
</CardHeader>
|
|
121
|
+
<CardContent>
|
|
122
|
+
<Input label="Client name" placeholder="Enter client name" />
|
|
123
|
+
<Button>Save client</Button>
|
|
124
|
+
</CardContent>
|
|
125
|
+
</Card>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Dark theme
|
|
131
|
+
|
|
132
|
+
Set `data-digicode-theme="dark"` on any parent to switch the token set:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
<div data-digicode-theme="dark">
|
|
136
|
+
<Button>Dark theme button</Button>
|
|
137
|
+
</div>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Customize the design system
|
|
141
|
+
|
|
142
|
+
Change the CSS variables at the top of `styles/digicode.css`:
|
|
143
|
+
|
|
144
|
+
```css
|
|
145
|
+
:root {
|
|
146
|
+
--digicode-primary: #2474ad;
|
|
147
|
+
--digicode-ink: #1c2f41;
|
|
148
|
+
--digicode-radius: 12px;
|
|
149
|
+
--digicode-radius-lg: 18px;
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Because the files are copied into the project, you can also modify each component directly.
|
|
154
|
+
|
|
155
|
+
## Package philosophy
|
|
156
|
+
|
|
157
|
+
This package intentionally excludes:
|
|
158
|
+
|
|
159
|
+
- complete pages
|
|
160
|
+
- hero sections
|
|
161
|
+
- website navigation
|
|
162
|
+
- footer sections
|
|
163
|
+
- DigiCode-specific copy/data
|
|
164
|
+
- API/enquiry logic
|
|
165
|
+
- app-specific dashboards
|
|
166
|
+
|
|
167
|
+
Only reusable UI primitives belong here.
|
package/bin/digicode.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
9
|
+
const registryRoot = path.join(packageRoot, "registry");
|
|
10
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(registryRoot, "registry.json"), "utf8"));
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const command = args[0];
|
|
13
|
+
const requested = args.slice(1);
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
|
|
16
|
+
function fail(message) {
|
|
17
|
+
console.error(`\nDigiCode: ${message}\n`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function detectSourceRoot() {
|
|
22
|
+
if (fs.existsSync(path.join(cwd, "src"))) return path.join(cwd, "src");
|
|
23
|
+
return cwd;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function paths() {
|
|
27
|
+
const sourceRoot = detectSourceRoot();
|
|
28
|
+
return {
|
|
29
|
+
sourceRoot,
|
|
30
|
+
componentsDir: path.join(sourceRoot, "components", "ui"),
|
|
31
|
+
libDir: path.join(sourceRoot, "lib"),
|
|
32
|
+
stylesDir: path.join(sourceRoot, "styles"),
|
|
33
|
+
configFile: path.join(cwd, "digicode.json")
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ensureDir(dir) {
|
|
38
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function copyIfMissing(source, destination, force = false) {
|
|
42
|
+
ensureDir(path.dirname(destination));
|
|
43
|
+
if (fs.existsSync(destination) && !force) return false;
|
|
44
|
+
fs.copyFileSync(source, destination);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function init() {
|
|
49
|
+
const p = paths();
|
|
50
|
+
ensureDir(p.componentsDir);
|
|
51
|
+
ensureDir(p.libDir);
|
|
52
|
+
ensureDir(p.stylesDir);
|
|
53
|
+
|
|
54
|
+
copyIfMissing(path.join(registryRoot, "lib", "cn.ts"), path.join(p.libDir, "cn.ts"));
|
|
55
|
+
copyIfMissing(path.join(registryRoot, "styles", "digicode.css"), path.join(p.stylesDir, "digicode.css"));
|
|
56
|
+
|
|
57
|
+
if (!fs.existsSync(p.configFile)) {
|
|
58
|
+
fs.writeFileSync(
|
|
59
|
+
p.configFile,
|
|
60
|
+
JSON.stringify(
|
|
61
|
+
{
|
|
62
|
+
sourceRoot: path.relative(cwd, p.sourceRoot) || ".",
|
|
63
|
+
components: path.relative(cwd, p.componentsDir),
|
|
64
|
+
styles: path.relative(cwd, path.join(p.stylesDir, "digicode.css")),
|
|
65
|
+
style: manifest.style
|
|
66
|
+
},
|
|
67
|
+
null,
|
|
68
|
+
2
|
|
69
|
+
) + "\n"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log("\nDigiCode initialized.");
|
|
74
|
+
console.log(`Components: ${path.relative(cwd, p.componentsDir)}`);
|
|
75
|
+
console.log(`Styles: ${path.relative(cwd, path.join(p.stylesDir, "digicode.css"))}`);
|
|
76
|
+
console.log("\nImport the stylesheet once in your root layout/main entry.");
|
|
77
|
+
if (p.sourceRoot.endsWith(`${path.sep}src`)) {
|
|
78
|
+
console.log('For Next.js src/app/layout.tsx: import "../styles/digicode.css";');
|
|
79
|
+
} else {
|
|
80
|
+
console.log('For app/layout.tsx: import "../styles/digicode.css";');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function add(names) {
|
|
85
|
+
if (!names.length) fail(`Specify components, for example: digicode add button input card`);
|
|
86
|
+
init();
|
|
87
|
+
|
|
88
|
+
const p = paths();
|
|
89
|
+
const targets = names.includes("all") ? manifest.components : names;
|
|
90
|
+
const unknown = targets.filter((name) => !manifest.components.includes(name));
|
|
91
|
+
if (unknown.length) fail(`Unknown component${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}`);
|
|
92
|
+
|
|
93
|
+
let copied = 0;
|
|
94
|
+
for (const name of targets) {
|
|
95
|
+
const source = path.join(registryRoot, "components", `${name}.tsx`);
|
|
96
|
+
const destination = path.join(p.componentsDir, `${name}.tsx`);
|
|
97
|
+
if (copyIfMissing(source, destination)) {
|
|
98
|
+
console.log(`+ ${path.relative(cwd, destination)}`);
|
|
99
|
+
copied++;
|
|
100
|
+
} else {
|
|
101
|
+
console.log(`= ${path.relative(cwd, destination)} (already exists)`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(`\n${copied} component${copied === 1 ? "" : "s"} added.`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function list() {
|
|
109
|
+
console.log("\nAvailable DigiCode components:\n");
|
|
110
|
+
for (const name of manifest.components) console.log(` - ${name}`);
|
|
111
|
+
console.log("");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function help() {
|
|
115
|
+
console.log(`\nDigiCode CLI\n\nUsage:\n digicode init\n digicode add button input card\n digicode add all\n digicode list\n\nWith npx after publishing:\n npx @indinno/digicode-ui init\n npx @indinno/digicode-ui add button input card\n`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
switch (command) {
|
|
119
|
+
case "init":
|
|
120
|
+
init();
|
|
121
|
+
break;
|
|
122
|
+
case "add":
|
|
123
|
+
add(requested);
|
|
124
|
+
break;
|
|
125
|
+
case "list":
|
|
126
|
+
list();
|
|
127
|
+
break;
|
|
128
|
+
case undefined:
|
|
129
|
+
case "help":
|
|
130
|
+
case "--help":
|
|
131
|
+
case "-h":
|
|
132
|
+
help();
|
|
133
|
+
break;
|
|
134
|
+
default:
|
|
135
|
+
fail(`Unknown command: ${command}`);
|
|
136
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@indinno/digicode-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DigiCode shadcn-style source-owned React UI primitives for reusable product interfaces.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"digicode": "./bin/digicode.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"registry",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"react",
|
|
18
|
+
"nextjs",
|
|
19
|
+
"ui",
|
|
20
|
+
"components",
|
|
21
|
+
"design-system",
|
|
22
|
+
"shadcn",
|
|
23
|
+
"cli"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
export interface AccordionItem {
|
|
6
|
+
value: string;
|
|
7
|
+
title: ReactNode;
|
|
8
|
+
content: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AccordionProps {
|
|
12
|
+
items: AccordionItem[];
|
|
13
|
+
defaultValue?: string;
|
|
14
|
+
allowCollapse?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Accordion({ items, defaultValue, allowCollapse = true, className }: AccordionProps) {
|
|
19
|
+
const [open, setOpen] = useState(defaultValue ?? "");
|
|
20
|
+
return (
|
|
21
|
+
<div className={className ? `digicode-accordion ${className}` : "digicode-accordion"}>
|
|
22
|
+
{items.map((item) => {
|
|
23
|
+
const expanded = open === item.value;
|
|
24
|
+
return (
|
|
25
|
+
<div className="digicode-accordion__item" key={item.value}>
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
className="digicode-accordion__trigger"
|
|
29
|
+
aria-expanded={expanded}
|
|
30
|
+
onClick={() => setOpen(expanded && allowCollapse ? "" : item.value)}
|
|
31
|
+
>
|
|
32
|
+
<span>{item.title}</span>
|
|
33
|
+
<span className="digicode-accordion__chevron" aria-hidden="true">⌄</span>
|
|
34
|
+
</button>
|
|
35
|
+
{expanded && <div className="digicode-accordion__content">{item.content}</div>}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
})}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type AlertVariant = "default" | "info" | "success" | "warning" | "danger";
|
|
5
|
+
export interface AlertProps extends HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
variant?: AlertVariant;
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
title?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Alert({ className, variant = "default", icon, title, children, ...props }: AlertProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div role="alert" className={cn("digicode-alert", variant !== "default" && `digicode-alert--${variant}`, className)} {...props}>
|
|
14
|
+
{icon && <span className="digicode-alert__icon">{icon}</span>}
|
|
15
|
+
<div>
|
|
16
|
+
{title && <p className="digicode-alert__title">{title}</p>}
|
|
17
|
+
<div className="digicode-alert__description">{children}</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HTMLAttributes, ImgHTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface AvatarProps extends HTMLAttributes<HTMLSpanElement> {
|
|
5
|
+
src?: string;
|
|
6
|
+
alt?: string;
|
|
7
|
+
fallback?: string;
|
|
8
|
+
size?: "sm" | "md" | "lg";
|
|
9
|
+
imageProps?: ImgHTMLAttributes<HTMLImageElement>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Avatar({ className, src, alt = "", fallback = "?", size = "md", imageProps, ...props }: AvatarProps) {
|
|
13
|
+
return (
|
|
14
|
+
<span className={cn("digicode-avatar", `digicode-avatar--${size}`, className)} {...props}>
|
|
15
|
+
{src ? <img src={src} alt={alt} {...imageProps} /> : fallback}
|
|
16
|
+
</span>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type BadgeVariant = "default" | "primary" | "success" | "warning" | "danger";
|
|
5
|
+
export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> { variant?: BadgeVariant; }
|
|
6
|
+
|
|
7
|
+
export function Badge({ className, variant = "default", ...props }: BadgeProps) {
|
|
8
|
+
return <span className={cn("digicode-badge", variant !== "default" && `digicode-badge--${variant}`, className)} {...props} />;
|
|
9
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface BreadcrumbItem {
|
|
4
|
+
label: ReactNode;
|
|
5
|
+
href?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Breadcrumb({ items, separator = "/", className }: { items: BreadcrumbItem[]; separator?: ReactNode; className?: string }) {
|
|
9
|
+
return (
|
|
10
|
+
<nav aria-label="Breadcrumb" className={className ? `digicode-breadcrumb ${className}` : "digicode-breadcrumb"}>
|
|
11
|
+
{items.map((item, index) => {
|
|
12
|
+
const current = index === items.length - 1;
|
|
13
|
+
return (
|
|
14
|
+
<span key={index} style={{ display: "contents" }}>
|
|
15
|
+
{current ? <span className="digicode-breadcrumb__current" aria-current="page">{item.label}</span> : item.href ? <a href={item.href}>{item.label}</a> : <span>{item.label}</span>}
|
|
16
|
+
{!current && <span className="digicode-breadcrumb__sep" aria-hidden="true">{separator}</span>}
|
|
17
|
+
</span>
|
|
18
|
+
);
|
|
19
|
+
})}
|
|
20
|
+
</nav>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "destructive" | "link";
|
|
5
|
+
export type ButtonSize = "sm" | "md" | "lg" | "icon";
|
|
6
|
+
|
|
7
|
+
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
8
|
+
variant?: ButtonVariant;
|
|
9
|
+
size?: ButtonSize;
|
|
10
|
+
loading?: boolean;
|
|
11
|
+
leftIcon?: ReactNode;
|
|
12
|
+
rightIcon?: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Button({
|
|
16
|
+
className,
|
|
17
|
+
variant = "primary",
|
|
18
|
+
size = "md",
|
|
19
|
+
loading = false,
|
|
20
|
+
leftIcon,
|
|
21
|
+
rightIcon,
|
|
22
|
+
disabled,
|
|
23
|
+
children,
|
|
24
|
+
...props
|
|
25
|
+
}: ButtonProps) {
|
|
26
|
+
return (
|
|
27
|
+
<button
|
|
28
|
+
className={cn("digicode-button", `digicode-button--${variant}`, size !== "md" && `digicode-button--${size}`, className)}
|
|
29
|
+
disabled={disabled || loading}
|
|
30
|
+
aria-busy={loading || undefined}
|
|
31
|
+
{...props}
|
|
32
|
+
>
|
|
33
|
+
{loading ? <span className="digicode-button__spinner" aria-hidden="true" /> : leftIcon}
|
|
34
|
+
{children}
|
|
35
|
+
{!loading && rightIcon}
|
|
36
|
+
</button>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface CardProps extends HTMLAttributes<HTMLDivElement> { elevated?: boolean; }
|
|
5
|
+
export function Card({ className, elevated = false, ...props }: CardProps) { return <div className={cn("digicode-card", elevated && "digicode-card--elevated", className)} {...props} />; }
|
|
6
|
+
export function CardHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) { return <div className={cn("digicode-card__header", className)} {...props} />; }
|
|
7
|
+
export function CardTitle({ className, ...props }: HTMLAttributes<HTMLHeadingElement>) { return <h3 className={cn("digicode-card__title", className)} {...props} />; }
|
|
8
|
+
export function CardDescription({ className, ...props }: HTMLAttributes<HTMLParagraphElement>) { return <p className={cn("digicode-card__description", className)} {...props} />; }
|
|
9
|
+
export function CardContent({ className, ...props }: HTMLAttributes<HTMLDivElement>) { return <div className={cn("digicode-card__content", className)} {...props} />; }
|
|
10
|
+
export function CardFooter({ className, ...props }: HTMLAttributes<HTMLDivElement>) { return <div className={cn("digicode-card__footer", className)} {...props} />; }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { forwardRef, type InputHTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
|
|
5
|
+
label?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(function Checkbox({ className, label, ...props }, ref) {
|
|
9
|
+
return (
|
|
10
|
+
<label className="digicode-checkline">
|
|
11
|
+
<input ref={ref} type="checkbox" className={cn("digicode-checkbox", className)} {...props} />
|
|
12
|
+
{label && <span>{label}</span>}
|
|
13
|
+
</label>
|
|
14
|
+
);
|
|
15
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
export interface DialogProps {
|
|
6
|
+
open: boolean;
|
|
7
|
+
onOpenChange: (open: boolean) => void;
|
|
8
|
+
title: ReactNode;
|
|
9
|
+
description?: ReactNode;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
footer?: ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Dialog({ open, onOpenChange, title, description, children, footer, className }: DialogProps) {
|
|
16
|
+
const ref = useRef<HTMLDialogElement>(null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const node = ref.current;
|
|
20
|
+
if (!node) return;
|
|
21
|
+
if (open && !node.open) node.showModal();
|
|
22
|
+
if (!open && node.open) node.close();
|
|
23
|
+
}, [open]);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<dialog
|
|
27
|
+
ref={ref}
|
|
28
|
+
className={className ? `digicode-dialog-native ${className}` : "digicode-dialog-native"}
|
|
29
|
+
onClose={() => onOpenChange(false)}
|
|
30
|
+
onCancel={(event) => {
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
onOpenChange(false);
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
<div className="digicode-dialog__header">
|
|
36
|
+
<div>
|
|
37
|
+
<h2 className="digicode-dialog__title">{title}</h2>
|
|
38
|
+
{description && <p className="digicode-dialog__description">{description}</p>}
|
|
39
|
+
</div>
|
|
40
|
+
<button type="button" className="digicode-dialog__close" aria-label="Close dialog" onClick={() => onOpenChange(false)}>×</button>
|
|
41
|
+
</div>
|
|
42
|
+
{children && <div className="digicode-dialog__body">{children}</div>}
|
|
43
|
+
{footer && <div className="digicode-dialog__footer">{footer}</div>}
|
|
44
|
+
</dialog>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
export interface DrawerProps {
|
|
6
|
+
open: boolean;
|
|
7
|
+
onOpenChange: (open: boolean) => void;
|
|
8
|
+
title: ReactNode;
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
footer?: ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Drawer({ open, onOpenChange, title, children, footer, className }: DrawerProps) {
|
|
15
|
+
const ref = useRef<HTMLDialogElement>(null);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const node = ref.current;
|
|
19
|
+
if (!node) return;
|
|
20
|
+
if (open && !node.open) node.showModal();
|
|
21
|
+
if (!open && node.open) node.close();
|
|
22
|
+
}, [open]);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<dialog
|
|
26
|
+
ref={ref}
|
|
27
|
+
className={className ? `digicode-drawer-native ${className}` : "digicode-drawer-native"}
|
|
28
|
+
onClose={() => onOpenChange(false)}
|
|
29
|
+
onCancel={(event) => {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
onOpenChange(false);
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
<div className="digicode-drawer__header">
|
|
35
|
+
<h2 className="digicode-dialog__title">{title}</h2>
|
|
36
|
+
<button type="button" className="digicode-dialog__close" aria-label="Close drawer" onClick={() => onOpenChange(false)}>×</button>
|
|
37
|
+
</div>
|
|
38
|
+
{children && <div className="digicode-drawer__body">{children}</div>}
|
|
39
|
+
{footer && <div className="digicode-drawer__footer">{footer}</div>}
|
|
40
|
+
</dialog>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, DetailsHTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface DropdownMenuProps extends DetailsHTMLAttributes<HTMLDetailsElement> {
|
|
5
|
+
trigger: ReactNode;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function DropdownMenu({ className, trigger, children, ...props }: DropdownMenuProps) {
|
|
10
|
+
return (
|
|
11
|
+
<details className={cn("digicode-dropdown", className)} {...props}>
|
|
12
|
+
<summary>{trigger}</summary>
|
|
13
|
+
<div className="digicode-dropdown__menu">{children}</div>
|
|
14
|
+
</details>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function DropdownMenuLabel({ children }: { children: ReactNode }) {
|
|
19
|
+
return <div className="digicode-dropdown__label">{children}</div>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function DropdownMenuSeparator() {
|
|
23
|
+
return <div className="digicode-dropdown__separator" role="separator" />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DropdownMenuItemProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
27
|
+
danger?: boolean;
|
|
28
|
+
icon?: ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function DropdownMenuItem({ className, danger = false, icon, children, ...props }: DropdownMenuItemProps) {
|
|
32
|
+
return (
|
|
33
|
+
<button type="button" className={cn("digicode-dropdown__item", danger && "digicode-dropdown__item--danger", className)} {...props}>
|
|
34
|
+
{icon}
|
|
35
|
+
<span>{children}</span>
|
|
36
|
+
</button>
|
|
37
|
+
);
|
|
38
|
+
}
|