@skutally/ui-library 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/README.md +514 -0
- package/bin/cli.js +272 -0
- package/package.json +36 -0
- package/registry/registry.json +207 -0
- package/src/base.css +82 -0
- package/src/controllers/accordion.js +27 -0
- package/src/controllers/alert.js +39 -0
- package/src/controllers/badge.js +40 -0
- package/src/controllers/button.js +79 -0
- package/src/controllers/card.js +10 -0
- package/src/controllers/checkbox.js +33 -0
- package/src/controllers/dropdown.js +35 -0
- package/src/controllers/input.js +24 -0
- package/src/controllers/modal.js +39 -0
- package/src/controllers/popover.js +35 -0
- package/src/controllers/progress.js +22 -0
- package/src/controllers/radio.js +30 -0
- package/src/controllers/sheet.js +37 -0
- package/src/controllers/slider.js +57 -0
- package/src/controllers/table.js +31 -0
- package/src/controllers/tabs.js +29 -0
- package/src/controllers/toast.js +41 -0
- package/src/controllers/toggle.js +27 -0
- package/src/controllers/tooltip.js +19 -0
- package/src/controllers.js +836 -0
- package/templates/components.json +13 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, existsSync, mkdirSync, copyFileSync } from "node:fs"
|
|
4
|
+
import { resolve, join, dirname, basename } from "node:path"
|
|
5
|
+
import { fileURLToPath } from "node:url"
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
8
|
+
const __dirname = dirname(__filename)
|
|
9
|
+
const PKG_ROOT = resolve(__dirname, "..")
|
|
10
|
+
|
|
11
|
+
// ─── Colors (no dependencies) ────────────────────────────────────────────────
|
|
12
|
+
const c = {
|
|
13
|
+
reset: "\x1b[0m",
|
|
14
|
+
bold: "\x1b[1m",
|
|
15
|
+
dim: "\x1b[2m",
|
|
16
|
+
green: "\x1b[32m",
|
|
17
|
+
cyan: "\x1b[36m",
|
|
18
|
+
yellow:"\x1b[33m",
|
|
19
|
+
red: "\x1b[31m",
|
|
20
|
+
gray: "\x1b[90m",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function success(msg) { console.log(`${c.green}✔${c.reset} ${msg}`) }
|
|
24
|
+
function info(msg) { console.log(`${c.cyan}ℹ${c.reset} ${msg}`) }
|
|
25
|
+
function warn(msg) { console.log(`${c.yellow}⚠${c.reset} ${msg}`) }
|
|
26
|
+
function error(msg) { console.error(`${c.red}✘${c.reset} ${msg}`); process.exit(1) }
|
|
27
|
+
|
|
28
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function loadRegistry() {
|
|
31
|
+
const registryPath = join(PKG_ROOT, "registry", "registry.json")
|
|
32
|
+
return JSON.parse(readFileSync(registryPath, "utf-8"))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loadConfig() {
|
|
36
|
+
const configPath = resolve(process.cwd(), "components.json")
|
|
37
|
+
if (!existsSync(configPath)) {
|
|
38
|
+
error("components.json not found. Run `ui-library init` first.")
|
|
39
|
+
}
|
|
40
|
+
return JSON.parse(readFileSync(configPath, "utf-8"))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ensureDir(dir) {
|
|
44
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function copyFile(src, dest) {
|
|
48
|
+
ensureDir(dirname(dest))
|
|
49
|
+
copyFileSync(src, dest)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
function cmdInit() {
|
|
55
|
+
const cwd = process.cwd()
|
|
56
|
+
const configDest = join(cwd, "components.json")
|
|
57
|
+
|
|
58
|
+
if (existsSync(configDest)) {
|
|
59
|
+
warn("components.json already exists. Skipping.")
|
|
60
|
+
} else {
|
|
61
|
+
copyFile(join(PKG_ROOT, "templates", "components.json"), configDest)
|
|
62
|
+
success("Created components.json")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Copy base CSS (Tailwind v4 — all config lives in CSS)
|
|
66
|
+
const cssDest = join(cwd, "src", "base.css")
|
|
67
|
+
if (!existsSync(cssDest)) {
|
|
68
|
+
copyFile(join(PKG_ROOT, "src", "base.css"), cssDest)
|
|
69
|
+
success("Created src/base.css (Tailwind v4 — @theme + CSS variables)")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log()
|
|
73
|
+
info("Project initialized with Tailwind CSS v4!")
|
|
74
|
+
console.log()
|
|
75
|
+
console.log(` ${c.dim}Build CSS:${c.reset} npx @tailwindcss/cli -i src/base.css -o dist/output.css --watch`)
|
|
76
|
+
console.log()
|
|
77
|
+
console.log(` ${c.dim}Add components:${c.reset}`)
|
|
78
|
+
console.log(` ${c.bold}npx ui-library add button${c.reset}`)
|
|
79
|
+
console.log(` ${c.bold}npx ui-library add modal accordion${c.reset}`)
|
|
80
|
+
console.log()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function cmdAdd(componentNames) {
|
|
84
|
+
if (componentNames.length === 0) {
|
|
85
|
+
error("Please specify at least one component. Example: ui-library add button")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const registry = loadRegistry()
|
|
89
|
+
const config = loadConfig()
|
|
90
|
+
const cwd = process.cwd()
|
|
91
|
+
const controllersDir = resolve(cwd, config.aliases.controllers)
|
|
92
|
+
const componentsDir = resolve(cwd, config.aliases.components)
|
|
93
|
+
|
|
94
|
+
// Resolve all dependencies
|
|
95
|
+
const toInstall = new Set()
|
|
96
|
+
|
|
97
|
+
function resolveDeps(name) {
|
|
98
|
+
if (toInstall.has(name)) return
|
|
99
|
+
const comp = registry.components[name]
|
|
100
|
+
if (!comp) {
|
|
101
|
+
warn(`Component "${name}" not found in registry. Skipping.`)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
for (const dep of comp.dependencies) {
|
|
105
|
+
resolveDeps(dep)
|
|
106
|
+
}
|
|
107
|
+
toInstall.add(name)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const name of componentNames) {
|
|
111
|
+
resolveDeps(name)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Install each component
|
|
115
|
+
let installed = 0
|
|
116
|
+
for (const name of toInstall) {
|
|
117
|
+
const comp = registry.components[name]
|
|
118
|
+
const files = comp.files
|
|
119
|
+
|
|
120
|
+
// Copy controller JS (if exists)
|
|
121
|
+
if (files.controller) {
|
|
122
|
+
const src = join(PKG_ROOT, files.controller)
|
|
123
|
+
const dest = join(controllersDir, basename(files.controller))
|
|
124
|
+
if (existsSync(dest)) {
|
|
125
|
+
warn(`${config.aliases.controllers}/${basename(files.controller)} already exists. Skipping.`)
|
|
126
|
+
} else {
|
|
127
|
+
copyFile(src, dest)
|
|
128
|
+
success(`Added ${c.bold}${config.aliases.controllers}/${basename(files.controller)}${c.reset}`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Copy HTML template
|
|
133
|
+
if (files.template) {
|
|
134
|
+
const src = join(PKG_ROOT, files.template)
|
|
135
|
+
const dest = join(componentsDir, basename(files.template))
|
|
136
|
+
if (existsSync(dest)) {
|
|
137
|
+
warn(`${config.aliases.components}/${basename(files.template)} already exists. Skipping.`)
|
|
138
|
+
} else {
|
|
139
|
+
copyFile(src, dest)
|
|
140
|
+
success(`Added ${c.bold}${config.aliases.components}/${basename(files.template)}${c.reset}`)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
installed++
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (installed > 0) {
|
|
148
|
+
console.log()
|
|
149
|
+
info(`Installed ${installed} component${installed > 1 ? "s" : ""}.`)
|
|
150
|
+
|
|
151
|
+
// Show registration hint
|
|
152
|
+
const controllers = [...toInstall]
|
|
153
|
+
.map(n => registry.components[n])
|
|
154
|
+
.filter(comp => comp.files.controller)
|
|
155
|
+
|
|
156
|
+
if (controllers.length > 0) {
|
|
157
|
+
console.log()
|
|
158
|
+
console.log(`${c.dim}Register the controller(s) in your application:${c.reset}`)
|
|
159
|
+
console.log()
|
|
160
|
+
for (const comp of controllers) {
|
|
161
|
+
const name = comp.name
|
|
162
|
+
const pascalName = name.charAt(0).toUpperCase() + name.slice(1)
|
|
163
|
+
console.log(` ${c.gray}import ${pascalName}Controller from "./${config.aliases.controllers}/${name}.js"${c.reset}`)
|
|
164
|
+
console.log(` ${c.gray}application.register("${name}", ${pascalName}Controller)${c.reset}`)
|
|
165
|
+
console.log()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function cmdList() {
|
|
172
|
+
const registry = loadRegistry()
|
|
173
|
+
const components = Object.values(registry.components)
|
|
174
|
+
|
|
175
|
+
console.log()
|
|
176
|
+
console.log(`${c.bold}Available components (${components.length}):${c.reset}`)
|
|
177
|
+
console.log()
|
|
178
|
+
|
|
179
|
+
const maxName = Math.max(...components.map(c => c.name.length))
|
|
180
|
+
|
|
181
|
+
for (const comp of components) {
|
|
182
|
+
const hasCtrl = comp.files.controller ? `${c.cyan}JS${c.reset}` : `${c.dim}--${c.reset}`
|
|
183
|
+
const deps = comp.dependencies.length > 0
|
|
184
|
+
? `${c.dim}deps: ${comp.dependencies.join(", ")}${c.reset}`
|
|
185
|
+
: ""
|
|
186
|
+
console.log(` ${c.green}${comp.name.padEnd(maxName + 2)}${c.reset} ${hasCtrl} ${comp.description} ${deps}`)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log()
|
|
190
|
+
console.log(`${c.dim}Usage: ui-library add <component> [component...]${c.reset}`)
|
|
191
|
+
console.log()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function cmdDiff(componentNames) {
|
|
195
|
+
if (componentNames.length === 0) {
|
|
196
|
+
error("Please specify at least one component. Example: ui-library diff button")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const registry = loadRegistry()
|
|
200
|
+
const config = loadConfig()
|
|
201
|
+
const cwd = process.cwd()
|
|
202
|
+
const controllersDir = resolve(cwd, config.aliases.controllers)
|
|
203
|
+
|
|
204
|
+
for (const name of componentNames) {
|
|
205
|
+
const comp = registry.components[name]
|
|
206
|
+
if (!comp) {
|
|
207
|
+
warn(`Component "${name}" not found in registry.`)
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
if (!comp.files.controller) {
|
|
211
|
+
info(`${name} has no controller file.`)
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const localPath = join(controllersDir, basename(comp.files.controller))
|
|
216
|
+
const registryPath = join(PKG_ROOT, comp.files.controller)
|
|
217
|
+
|
|
218
|
+
if (!existsSync(localPath)) {
|
|
219
|
+
info(`${name}: not installed locally.`)
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const local = readFileSync(localPath, "utf-8")
|
|
224
|
+
const remote = readFileSync(registryPath, "utf-8")
|
|
225
|
+
|
|
226
|
+
if (local === remote) {
|
|
227
|
+
success(`${name}: no differences.`)
|
|
228
|
+
} else {
|
|
229
|
+
warn(`${name}: local version differs from registry.`)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ─── CLI Router ──────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
const [,, command, ...args] = process.argv
|
|
237
|
+
|
|
238
|
+
switch (command) {
|
|
239
|
+
case "init":
|
|
240
|
+
cmdInit()
|
|
241
|
+
break
|
|
242
|
+
case "add":
|
|
243
|
+
cmdAdd(args)
|
|
244
|
+
break
|
|
245
|
+
case "list":
|
|
246
|
+
cmdList()
|
|
247
|
+
break
|
|
248
|
+
case "diff":
|
|
249
|
+
cmdDiff(args)
|
|
250
|
+
break
|
|
251
|
+
case "help":
|
|
252
|
+
case "--help":
|
|
253
|
+
case "-h":
|
|
254
|
+
case undefined:
|
|
255
|
+
console.log(`
|
|
256
|
+
${c.bold}ui-library${c.reset} — shadcn-style UI components for Stimulus + Tailwind
|
|
257
|
+
|
|
258
|
+
${c.bold}Usage:${c.reset}
|
|
259
|
+
ui-library init Initialize project (creates components.json, base CSS, tailwind config)
|
|
260
|
+
ui-library add <name> [name...] Add component(s) to your project
|
|
261
|
+
ui-library list List all available components
|
|
262
|
+
ui-library diff <name> Check if a local component differs from the registry
|
|
263
|
+
|
|
264
|
+
${c.bold}Examples:${c.reset}
|
|
265
|
+
ui-library init
|
|
266
|
+
ui-library add button modal accordion
|
|
267
|
+
ui-library list
|
|
268
|
+
`)
|
|
269
|
+
break
|
|
270
|
+
default:
|
|
271
|
+
error(`Unknown command: "${command}". Run ui-library --help for usage.`)
|
|
272
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@skutally/ui-library",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A shadcn-style UI component library for HTML, Tailwind CSS & Stimulus.js. Copy-paste components into your project.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ui-library": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src",
|
|
15
|
+
"registry",
|
|
16
|
+
"templates"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"ui",
|
|
20
|
+
"components",
|
|
21
|
+
"tailwindcss",
|
|
22
|
+
"stimulus",
|
|
23
|
+
"shadcn"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"dev": "npx @tailwindcss/cli -i src/base.css -o dist/output.css --watch",
|
|
27
|
+
"build": "npx @tailwindcss/cli -i src/base.css -o dist/output.css --minify",
|
|
28
|
+
"serve": "npx serve .",
|
|
29
|
+
"start": "npm run build && npx serve ."
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@tailwindcss/cli": "^4.2.2",
|
|
34
|
+
"tailwindcss": "^4.2.2"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
{
|
|
2
|
+
"components": {
|
|
3
|
+
"accordion": {
|
|
4
|
+
"name": "accordion",
|
|
5
|
+
"description": "A vertically stacked set of interactive headings that each reveal a section of content.",
|
|
6
|
+
"dependencies": [],
|
|
7
|
+
"files": {
|
|
8
|
+
"controller": "src/controllers/accordion.js",
|
|
9
|
+
"template": "components/accordion.html"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"alert": {
|
|
13
|
+
"name": "alert",
|
|
14
|
+
"description": "Displays a callout for important information with variant styles (info, success, warning, danger).",
|
|
15
|
+
"dependencies": [],
|
|
16
|
+
"files": {
|
|
17
|
+
"controller": "src/controllers/alert.js",
|
|
18
|
+
"template": "components/alert.html"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"avatar": {
|
|
22
|
+
"name": "avatar",
|
|
23
|
+
"description": "An image element with a fallback for representing the user.",
|
|
24
|
+
"dependencies": [],
|
|
25
|
+
"files": {
|
|
26
|
+
"template": "components/avatar.html"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"badge": {
|
|
30
|
+
"name": "badge",
|
|
31
|
+
"description": "Displays a small status descriptor with variant styles.",
|
|
32
|
+
"dependencies": [],
|
|
33
|
+
"files": {
|
|
34
|
+
"controller": "src/controllers/badge.js",
|
|
35
|
+
"template": "components/badge.html"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"breadcrumb": {
|
|
39
|
+
"name": "breadcrumb",
|
|
40
|
+
"description": "Displays the path to the current page using a hierarchy of links.",
|
|
41
|
+
"dependencies": [],
|
|
42
|
+
"files": {
|
|
43
|
+
"template": "components/breadcrumb.html"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"button": {
|
|
47
|
+
"name": "button",
|
|
48
|
+
"description": "A clickable button with multiple variants and sizes.",
|
|
49
|
+
"dependencies": [],
|
|
50
|
+
"files": {
|
|
51
|
+
"controller": "src/controllers/button.js",
|
|
52
|
+
"template": "components/button.html"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"card": {
|
|
56
|
+
"name": "card",
|
|
57
|
+
"description": "A container with a border and shadow used to group content.",
|
|
58
|
+
"dependencies": [],
|
|
59
|
+
"files": {
|
|
60
|
+
"controller": "src/controllers/card.js",
|
|
61
|
+
"template": "components/card.html"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"checkbox": {
|
|
65
|
+
"name": "checkbox",
|
|
66
|
+
"description": "A control that allows the user to toggle between checked and not checked.",
|
|
67
|
+
"dependencies": [],
|
|
68
|
+
"files": {
|
|
69
|
+
"controller": "src/controllers/checkbox.js",
|
|
70
|
+
"template": "components/checkbox.html"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"dropdown": {
|
|
74
|
+
"name": "dropdown",
|
|
75
|
+
"description": "A menu that appears on trigger click with a list of actions.",
|
|
76
|
+
"dependencies": [],
|
|
77
|
+
"files": {
|
|
78
|
+
"controller": "src/controllers/dropdown.js",
|
|
79
|
+
"template": "components/dropdown.html"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"input": {
|
|
83
|
+
"name": "input",
|
|
84
|
+
"description": "A text input field with variant styles and icon support.",
|
|
85
|
+
"dependencies": [],
|
|
86
|
+
"files": {
|
|
87
|
+
"controller": "src/controllers/input.js",
|
|
88
|
+
"template": "components/input.html"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"modal": {
|
|
92
|
+
"name": "modal",
|
|
93
|
+
"description": "A dialog overlay that requires user interaction before returning to the page.",
|
|
94
|
+
"dependencies": ["button"],
|
|
95
|
+
"files": {
|
|
96
|
+
"controller": "src/controllers/modal.js",
|
|
97
|
+
"template": "components/modal.html"
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"popover": {
|
|
101
|
+
"name": "popover",
|
|
102
|
+
"description": "A floating panel anchored to a trigger element.",
|
|
103
|
+
"dependencies": [],
|
|
104
|
+
"files": {
|
|
105
|
+
"controller": "src/controllers/popover.js",
|
|
106
|
+
"template": "components/popover.html"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"progress": {
|
|
110
|
+
"name": "progress",
|
|
111
|
+
"description": "A horizontal bar indicating progress or completion percentage.",
|
|
112
|
+
"dependencies": [],
|
|
113
|
+
"files": {
|
|
114
|
+
"controller": "src/controllers/progress.js",
|
|
115
|
+
"template": "components/progress.html"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"radio": {
|
|
119
|
+
"name": "radio",
|
|
120
|
+
"description": "A set of mutually exclusive options presented as a group.",
|
|
121
|
+
"dependencies": [],
|
|
122
|
+
"files": {
|
|
123
|
+
"controller": "src/controllers/radio.js",
|
|
124
|
+
"template": "components/radio.html"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"separator": {
|
|
128
|
+
"name": "separator",
|
|
129
|
+
"description": "A visual divider between sections of content.",
|
|
130
|
+
"dependencies": [],
|
|
131
|
+
"files": {
|
|
132
|
+
"template": "components/separator.html"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
"sheet": {
|
|
136
|
+
"name": "sheet",
|
|
137
|
+
"description": "A panel that slides in from any edge of the screen.",
|
|
138
|
+
"dependencies": ["button"],
|
|
139
|
+
"files": {
|
|
140
|
+
"controller": "src/controllers/sheet.js",
|
|
141
|
+
"template": "components/sheet.html"
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"skeleton": {
|
|
145
|
+
"name": "skeleton",
|
|
146
|
+
"description": "A placeholder animation shown while content is loading.",
|
|
147
|
+
"dependencies": [],
|
|
148
|
+
"files": {
|
|
149
|
+
"template": "components/skeleton.html"
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"slider": {
|
|
153
|
+
"name": "slider",
|
|
154
|
+
"description": "A draggable input for selecting a numeric value within a range.",
|
|
155
|
+
"dependencies": [],
|
|
156
|
+
"files": {
|
|
157
|
+
"controller": "src/controllers/slider.js",
|
|
158
|
+
"template": "components/slider.html"
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"table": {
|
|
162
|
+
"name": "table",
|
|
163
|
+
"description": "A data table with sortable column headers.",
|
|
164
|
+
"dependencies": [],
|
|
165
|
+
"files": {
|
|
166
|
+
"controller": "src/controllers/table.js",
|
|
167
|
+
"template": "components/table.html"
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
"tabs": {
|
|
171
|
+
"name": "tabs",
|
|
172
|
+
"description": "A set of layered sections of content that display one panel at a time.",
|
|
173
|
+
"dependencies": [],
|
|
174
|
+
"files": {
|
|
175
|
+
"controller": "src/controllers/tabs.js",
|
|
176
|
+
"template": "components/tabs.html"
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
"toast": {
|
|
180
|
+
"name": "toast",
|
|
181
|
+
"description": "A temporary notification that appears at the edge of the screen.",
|
|
182
|
+
"dependencies": [],
|
|
183
|
+
"files": {
|
|
184
|
+
"controller": "src/controllers/toast.js",
|
|
185
|
+
"template": "components/toast.html"
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
"toggle": {
|
|
189
|
+
"name": "toggle",
|
|
190
|
+
"description": "A switch control that toggles between on and off states.",
|
|
191
|
+
"dependencies": [],
|
|
192
|
+
"files": {
|
|
193
|
+
"controller": "src/controllers/toggle.js",
|
|
194
|
+
"template": "components/toggle.html"
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
"tooltip": {
|
|
198
|
+
"name": "tooltip",
|
|
199
|
+
"description": "A small popup that shows additional information on hover.",
|
|
200
|
+
"dependencies": [],
|
|
201
|
+
"files": {
|
|
202
|
+
"controller": "src/controllers/tooltip.js",
|
|
203
|
+
"template": "components/tooltip.html"
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
package/src/base.css
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
4
|
+
|
|
5
|
+
@theme {
|
|
6
|
+
--font-sans: "Inter", "system-ui", "-apple-system", "sans-serif";
|
|
7
|
+
--font-mono: "JetBrains Mono", "Fira Code", "monospace";
|
|
8
|
+
|
|
9
|
+
--color-border: hsl(var(--border));
|
|
10
|
+
--color-input: hsl(var(--input));
|
|
11
|
+
--color-ring: hsl(var(--ring));
|
|
12
|
+
--color-background: hsl(var(--background));
|
|
13
|
+
--color-foreground: hsl(var(--foreground));
|
|
14
|
+
|
|
15
|
+
--color-primary: hsl(var(--primary));
|
|
16
|
+
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
17
|
+
|
|
18
|
+
--color-muted: hsl(var(--muted));
|
|
19
|
+
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
20
|
+
|
|
21
|
+
--color-accent: hsl(var(--accent));
|
|
22
|
+
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
23
|
+
|
|
24
|
+
--color-destructive: hsl(var(--destructive));
|
|
25
|
+
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
26
|
+
|
|
27
|
+
--color-card: hsl(var(--card));
|
|
28
|
+
--color-card-foreground: hsl(var(--card-foreground));
|
|
29
|
+
|
|
30
|
+
--radius-lg: var(--radius);
|
|
31
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
32
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@layer base {
|
|
36
|
+
:root {
|
|
37
|
+
--background: 0 0% 100%;
|
|
38
|
+
--foreground: 240 10% 3.9%;
|
|
39
|
+
--card: 0 0% 100%;
|
|
40
|
+
--card-foreground: 240 10% 3.9%;
|
|
41
|
+
--primary: 240 5.9% 10%;
|
|
42
|
+
--primary-foreground: 0 0% 98%;
|
|
43
|
+
--muted: 240 4.8% 95.9%;
|
|
44
|
+
--muted-foreground: 240 3.8% 46.1%;
|
|
45
|
+
--accent: 240 4.8% 95.9%;
|
|
46
|
+
--accent-foreground: 240 5.9% 10%;
|
|
47
|
+
--destructive: 0 84.2% 60.2%;
|
|
48
|
+
--destructive-foreground: 0 0% 98%;
|
|
49
|
+
--border: 240 5.9% 90%;
|
|
50
|
+
--input: 240 5.9% 90%;
|
|
51
|
+
--ring: 240 5.9% 10%;
|
|
52
|
+
--radius: 0.5rem;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.dark {
|
|
56
|
+
--background: 240 10% 3.9%;
|
|
57
|
+
--foreground: 0 0% 98%;
|
|
58
|
+
--card: 240 10% 3.9%;
|
|
59
|
+
--card-foreground: 0 0% 98%;
|
|
60
|
+
--primary: 0 0% 98%;
|
|
61
|
+
--primary-foreground: 240 5.9% 10%;
|
|
62
|
+
--muted: 240 3.7% 15.9%;
|
|
63
|
+
--muted-foreground: 240 5% 64.9%;
|
|
64
|
+
--accent: 240 3.7% 15.9%;
|
|
65
|
+
--accent-foreground: 0 0% 98%;
|
|
66
|
+
--destructive: 0 62.8% 30.6%;
|
|
67
|
+
--destructive-foreground: 0 0% 98%;
|
|
68
|
+
--border: 240 3.7% 15.9%;
|
|
69
|
+
--input: 240 3.7% 15.9%;
|
|
70
|
+
--ring: 240 4.9% 83.9%;
|
|
71
|
+
--radius: 0.5rem;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
* {
|
|
75
|
+
border-color: hsl(var(--border));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
body {
|
|
79
|
+
background-color: hsl(var(--background));
|
|
80
|
+
color: hsl(var(--foreground));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["item", "body", "icon"]
|
|
5
|
+
|
|
6
|
+
connect() {
|
|
7
|
+
this.openIndex = -1
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
toggle(e) {
|
|
11
|
+
const button = e.currentTarget
|
|
12
|
+
const item = button.closest("[data-accordion-item]")
|
|
13
|
+
const body = item.querySelector("[data-accordion-target='body']")
|
|
14
|
+
const icon = item.querySelector("[data-accordion-target='icon']")
|
|
15
|
+
const isOpen = !body.classList.contains("hidden")
|
|
16
|
+
|
|
17
|
+
// Close all
|
|
18
|
+
this.element.querySelectorAll("[data-accordion-target='body']").forEach(b => b.classList.add("hidden"))
|
|
19
|
+
this.element.querySelectorAll("[data-accordion-target='icon']").forEach(i => i.style.transform = "")
|
|
20
|
+
|
|
21
|
+
// Open clicked if was closed
|
|
22
|
+
if (!isOpen) {
|
|
23
|
+
body.classList.remove("hidden")
|
|
24
|
+
if (icon) icon.style.transform = "rotate(180deg)"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static values = {
|
|
5
|
+
variant: { type: String, default: "info" },
|
|
6
|
+
dismissible: { type: Boolean, default: true },
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
connect() {
|
|
10
|
+
this.applyVariant()
|
|
11
|
+
if (this.dismissibleValue) this.addCloseButton()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
applyVariant() {
|
|
15
|
+
const base = "relative flex items-start gap-3 px-4 py-3 rounded-lg border text-sm transition-all [&>svg]:shrink-0 [&>svg]:mt-0.5"
|
|
16
|
+
const map = {
|
|
17
|
+
info: "bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-950 dark:border-blue-900 dark:text-blue-200",
|
|
18
|
+
success: "bg-emerald-50 border-emerald-200 text-emerald-800 dark:bg-emerald-950 dark:border-emerald-900 dark:text-emerald-200",
|
|
19
|
+
warning: "bg-amber-50 border-amber-200 text-amber-800 dark:bg-amber-950 dark:border-amber-900 dark:text-amber-200",
|
|
20
|
+
danger: "bg-red-50 border-red-200 text-red-800 dark:bg-red-950 dark:border-red-900 dark:text-red-200",
|
|
21
|
+
}
|
|
22
|
+
this.element.className = `${base} ${map[this.variantValue] ?? map.info}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
addCloseButton() {
|
|
26
|
+
if (this.element.querySelector("[data-alert-close]")) return
|
|
27
|
+
const btn = document.createElement("button")
|
|
28
|
+
btn.setAttribute("data-alert-close", "")
|
|
29
|
+
btn.className = "absolute top-3 right-3 text-lg leading-none opacity-40 hover:opacity-80 cursor-pointer"
|
|
30
|
+
btn.textContent = "\u2715"
|
|
31
|
+
btn.addEventListener("click", () => this.dismiss())
|
|
32
|
+
this.element.appendChild(btn)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
dismiss() {
|
|
36
|
+
this.element.classList.add("opacity-0", "-translate-y-1")
|
|
37
|
+
setTimeout(() => this.element.remove(), 200)
|
|
38
|
+
}
|
|
39
|
+
}
|