@energycap/internal-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/README.md +77 -0
- package/bin/cli.mjs +79 -0
- package/dist/internal-ui.css +2 -0
- package/package.json +43 -0
- package/skills/energycap-internal-ui/SKILL.md +89 -0
- package/skills/energycap-internal-ui/reference/components.md +166 -0
- package/skills/energycap-internal-ui/reference/page-templates.md +105 -0
- package/src/base.css +38 -0
- package/src/components/alerts.css +10 -0
- package/src/components/badges.css +11 -0
- package/src/components/buttons.css +26 -0
- package/src/components/cards.css +12 -0
- package/src/components/forms.css +28 -0
- package/src/components/layout.css +49 -0
- package/src/components/tables.css +50 -0
- package/src/dist-entry.css +13 -0
- package/src/index.css +19 -0
- package/src/theme.css +83 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @energycap/internal-ui
|
|
2
|
+
|
|
3
|
+
Shared Tailwind v4 theme + component classes for EnergyCAP internal tools.
|
|
4
|
+
Framework-agnostic — works with any UI framework or server-rendered HTML; bring
|
|
5
|
+
your own markup (Razor, React, plain HTML) and apply the classes. Components are
|
|
6
|
+
built Tailwind-native (`@apply`), so sizing and spacing track Tailwind's scale
|
|
7
|
+
and the build path requires Tailwind v4 — the prebuilt `dist` is standalone CSS.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @energycap/internal-ui
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Consume
|
|
16
|
+
|
|
17
|
+
**Tailwind v4 build** (npm + PostCSS, Vite, etc.):
|
|
18
|
+
|
|
19
|
+
```css
|
|
20
|
+
@import "tailwindcss";
|
|
21
|
+
@import "@energycap/internal-ui";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**No build** — link the precompiled bundle (reset + tokens + components, no
|
|
25
|
+
utility layer):
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<link rel="stylesheet" href="node_modules/@energycap/internal-ui/dist/internal-ui.css" />
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Design tokens
|
|
32
|
+
|
|
33
|
+
All tokens are CSS variables on `:root` (so any CSS can use `var(--…)`), and the
|
|
34
|
+
color tokens are also exposed as Tailwind utilities.
|
|
35
|
+
|
|
36
|
+
| Variable | Utility | Value |
|
|
37
|
+
|----------|---------|-------|
|
|
38
|
+
| `--primary` / `--primary-hover` | `bg-primary` / `bg-primary-hover`, `text-primary` | indigo 600 / 700 |
|
|
39
|
+
| `--canvas` | `bg-canvas` | slate-50 (page background) |
|
|
40
|
+
| `--surface` / `--surface-alt` | `bg-surface` / `bg-surface-alt` | white / slate-100 |
|
|
41
|
+
| `--border` / `--border-strong` | `border-border` / `border-border-strong` | slate-200 / slate-400 |
|
|
42
|
+
| `--ink` / `--ink-soft` | `text-ink` / `text-ink-soft` | slate-900 / slate-800 |
|
|
43
|
+
| `--muted` / `--muted-strong` | `text-muted` / `text-muted-strong` | slate-500 / slate-600 |
|
|
44
|
+
| `--success` / `--warning` / `--danger` / `--info` | `text-*` / `bg-*` | green-600 / amber-600 / red-600 / blue-500 |
|
|
45
|
+
| `--danger-hover` | `bg-danger-hover` | red-700 (solid danger button hover) |
|
|
46
|
+
| `--{success,warning,danger,info}-bg` / `-text` | — | soft fill + readable text for alerts/badges |
|
|
47
|
+
| `--radius-sm/md/lg` | `rounded-sm/md/lg` | 4 / 6 / 8 px (match Tailwind defaults) |
|
|
48
|
+
| `--shadow-card` | — | card / panel elevation |
|
|
49
|
+
| `--font-sans` / `--font-mono` | `font-sans` / `font-mono` | Inter / Cascadia Code |
|
|
50
|
+
|
|
51
|
+
## Component classes
|
|
52
|
+
|
|
53
|
+
| Family | Classes |
|
|
54
|
+
|--------|---------|
|
|
55
|
+
| Buttons | `.btn` + `.btn-primary` / `.btn-outline` / `.btn-danger`, `.btn-sm`, `.btn-icon-only`, `.btn-group` |
|
|
56
|
+
| Forms | `.form-group` (`> label`), `.form-control` (input/select/textarea), `.form-hint`, `.form-actions` |
|
|
57
|
+
| Cards | `.card`, `.card-heading` |
|
|
58
|
+
| Tables | `.table-container` (+ `.table-sticky`) wrapping a `<table>`; `.table-empty`; `.row-clickable` + `.row-anchor` |
|
|
59
|
+
| Badges | `.badge` + `.badge-success/warning/danger/info/muted` |
|
|
60
|
+
| Alerts | `.alert` + `.alert-success/warning/error/info` (`.alert-danger` alias) |
|
|
61
|
+
| Layout | `.app-shell`, `.sidebar` (`.sidebar-brand/-nav/-footer`, `a.active`, `.nav-section`), `.main-content`, `.content-header`, `.subtitle`, `.toolbar`, `.pagination` |
|
|
62
|
+
|
|
63
|
+
See the kitchen-sink docs site (`apps/docs`) for live examples of every token
|
|
64
|
+
and component.
|
|
65
|
+
|
|
66
|
+
## Agent skills
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx @energycap/internal-ui install-skills # -> ./.agents/skills
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Build
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm run build # compile dist/internal-ui.css (minified)
|
|
76
|
+
npm run build:debug # same, unminified for inspection
|
|
77
|
+
```
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* @energycap/internal-ui CLI — currently just installs the bundled agent skills
|
|
3
|
+
into a consuming repo's agent-agnostic .agents/skills directory. */
|
|
4
|
+
import { cp, mkdir, readdir } from 'node:fs/promises';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const PKG_ROOT = resolve(here, '..');
|
|
11
|
+
|
|
12
|
+
const [, , command, ...rest] = process.argv;
|
|
13
|
+
|
|
14
|
+
function parseFlags(args) {
|
|
15
|
+
const flags = {};
|
|
16
|
+
for (const a of args) {
|
|
17
|
+
if (a.startsWith('--')) {
|
|
18
|
+
const [k, v] = a.slice(2).split('=');
|
|
19
|
+
flags[k] = v ?? true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return flags;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function installSkills(args) {
|
|
26
|
+
const flags = parseFlags(args);
|
|
27
|
+
const src = join(PKG_ROOT, 'skills');
|
|
28
|
+
const dest = resolve(process.cwd(), flags.dest || '.agents/skills');
|
|
29
|
+
|
|
30
|
+
if (!existsSync(src)) {
|
|
31
|
+
console.error(`[internal-ui] no skills bundled in this package (looked in ${src}).`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const entries = (await readdir(src, { withFileTypes: true })).filter((e) => e.isDirectory());
|
|
36
|
+
if (entries.length === 0) {
|
|
37
|
+
console.error('[internal-ui] no skills to install.');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await mkdir(dest, { recursive: true });
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const target = join(dest, entry.name);
|
|
44
|
+
if (existsSync(target) && !flags.force) {
|
|
45
|
+
console.log(` skip ${entry.name} (already present — pass --force to overwrite)`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
await cp(join(src, entry.name), target, { recursive: true, force: true });
|
|
49
|
+
console.log(` copied ${entry.name}`);
|
|
50
|
+
}
|
|
51
|
+
console.log(`\n[internal-ui] skills installed to ${dest}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function usage() {
|
|
55
|
+
console.log(`@energycap/internal-ui
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
npx @energycap/internal-ui install-skills [--dest=<dir>] [--force]
|
|
59
|
+
|
|
60
|
+
Commands:
|
|
61
|
+
install-skills Copy the bundled agent skills into <dir> (default: .agents/skills)
|
|
62
|
+
`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
switch (command) {
|
|
66
|
+
case 'install-skills':
|
|
67
|
+
await installSkills(rest);
|
|
68
|
+
break;
|
|
69
|
+
case undefined:
|
|
70
|
+
case 'help':
|
|
71
|
+
case '--help':
|
|
72
|
+
case '-h':
|
|
73
|
+
usage();
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
console.error(`Unknown command: ${command}\n`);
|
|
77
|
+
usage();
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
|
|
2
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-leading:initial;--tw-font-weight:initial;--tw-border-style:solid;--tw-tracking:initial;--tw-outline-style:solid}}}@layer theme{:root,:host{--font-sans:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--font-mono:"Cascadia Code", "Fira Code", "Consolas", monospace;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--tracking-wider:.05em;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}body{font-family:var(--font-sans,"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);color:var(--ink);background:var(--canvas);-webkit-font-smoothing:antialiased;line-height:1.5}ol{padding-left:1.5em;list-style:decimal}ul{padding-left:1.5em;list-style:outside}:focus{outline:none}:focus-visible{outline:2px solid var(--primary);outline-offset:2px;border-radius:3px}}@layer components{.btn{cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 1.5);border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-normal);line-height:var(--leading-normal);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-white);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));text-decoration-line:none;display:inline-flex}.btn:hover{color:var(--color-white);text-decoration-line:none}.btn:disabled{cursor:not-allowed;opacity:.6}.btn:focus-visible{--tw-outline-style:none;box-shadow:0 0 0 3px var(--primary-soft);outline-style:none}.btn-primary{background-color:var(--primary)}.btn-primary:hover{background-color:var(--primary-hover)}.btn-danger{background-color:var(--danger)}.btn-danger:hover{background-color:var(--danger-hover)}.btn-outline{border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface);color:var(--ink)}.btn-outline:hover{border-color:var(--border-strong);background-color:var(--canvas);color:var(--ink)}.btn-sm{padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.btn-icon-only{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8);padding:0}.btn-group{align-items:center;gap:calc(var(--spacing) * 1.5);display:inline-flex}.form-group{margin-bottom:calc(var(--spacing) * 5)}.form-group>label{margin-bottom:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--ink);display:block}.form-control{border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--ink);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));font-family:inherit}.form-control::placeholder{color:var(--border-strong)}.form-control:focus{border-color:var(--primary);--tw-outline-style:none;box-shadow:0 0 0 3px var(--primary-soft);outline-style:none}textarea.form-control{min-height:calc(var(--spacing) * 30);resize:vertical;font-family:var(--font-mono);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}select.form-control{cursor:pointer;appearance:none;padding-right:calc(var(--spacing) * 9);--tw-leading:var(--leading-snug);line-height:var(--leading-snug);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='7'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%2394a3b8' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-position:right 12px center;background-repeat:no-repeat}.form-hint{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted)}.form-actions{margin-top:calc(var(--spacing) * 6);gap:calc(var(--spacing) * 2.5);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border);padding-top:calc(var(--spacing) * 4);display:flex}.card{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 5);box-shadow:var(--shadow-card)}.card-heading{margin-bottom:calc(var(--spacing) * 4);align-items:center;gap:calc(var(--spacing) * 2);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding-bottom:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--ink-soft);display:flex}.card-heading i{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted)}.table-container{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface);max-width:100%;box-shadow:var(--shadow-card);overflow:auto}.table-container table{border-collapse:collapse;width:100%}.table-container thead th{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);background-color:var(--surface-alt);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);text-align:left;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide);color:var(--muted);text-transform:uppercase}.table-container tbody td{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);vertical-align:middle;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.table-container tbody tr:last-child td{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.table-container tbody tr:hover{background-color:var(--canvas)}.table-container tfoot td{border-top-style:var(--tw-border-style);border-top-width:2px;border-color:var(--border);background-color:var(--canvas);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--ink-soft)}.table-empty{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 10);text-align:center;color:var(--muted)}.table-sticky thead th{z-index:2;background-color:var(--surface-alt);box-shadow:inset 0 -1px 0 var(--border);position:sticky;top:0}.row-clickable{cursor:pointer;position:relative}.row-clickable:focus-within{background-color:var(--canvas)}.row-anchor{z-index:1;text-indent:-9999px;color:inherit;position:absolute;inset:0;overflow:hidden}.row-anchor:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-color:var(--primary);outline-offset:-2px;border-radius:2px}.badge{align-items:center;gap:var(--spacing);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);white-space:nowrap;border-radius:3.40282e38px;display:inline-flex}.badge-success{background:var(--success-bg);color:var(--success-text)}.badge-warning{background:var(--warning-bg);color:var(--warning-text)}.badge-danger{background:var(--danger-bg);color:var(--danger-text)}.badge-info{background:var(--info-bg);color:var(--info-text)}.badge-muted{background-color:var(--surface-alt);color:var(--muted-strong)}.alert{margin-bottom:calc(var(--spacing) * 4);align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);display:flex}.alert-success{border-style:var(--tw-border-style);border-width:1px;border-color:var(--success);background:var(--success-bg);color:var(--success-text)}.alert-warning{border-style:var(--tw-border-style);border-width:1px;border-color:var(--warning);background:var(--warning-bg);color:var(--warning-text)}.alert-error,.alert-danger{border-style:var(--tw-border-style);border-width:1px;border-color:var(--danger);background:var(--danger-bg);color:var(--danger-text)}.alert-info{border-style:var(--tw-border-style);border-width:1px;border-color:var(--info);background:var(--info-bg);color:var(--info-text)}.app-shell{min-height:100dvh;display:flex}.sidebar{width:calc(var(--spacing) * 55);background-color:var(--ink);flex-direction:column;flex-shrink:0;display:flex}.sidebar-brand{align-items:center;gap:calc(var(--spacing) * 2.5);border-bottom-style:var(--tw-border-style);border-color:#ffffff1a;border-bottom-width:1px;display:flex}@supports (color:color-mix(in lab, red, red)){.sidebar-brand{border-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.sidebar-brand{padding-inline:calc(var(--spacing) * 4.5);padding-block:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide);color:var(--color-white)}.sidebar-nav{padding-inline:0;padding-block:calc(var(--spacing) * 2);flex:1;margin:0;list-style-type:none;overflow-y:auto}.sidebar-nav .nav-section{padding-inline:calc(var(--spacing) * 4.5);padding-top:calc(var(--spacing) * 4);padding-bottom:calc(var(--spacing) * 1.5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);color:var(--muted-strong);text-transform:uppercase}.sidebar-nav a{align-items:center;gap:calc(var(--spacing) * 2.5);border-left-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 4.5);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--border-strong);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-color:#0000;border-left-width:3px;text-decoration-line:none;display:flex}.sidebar-nav a:hover{background-color:#ffffff0f}@supports (color:color-mix(in lab, red, red)){.sidebar-nav a:hover{background-color:color-mix(in oklab, var(--color-white) 6%, transparent)}}.sidebar-nav a:hover{color:var(--color-white)}.sidebar-nav a.active{background-color:#ffffff0f}@supports (color:color-mix(in lab, red, red)){.sidebar-nav a.active{background-color:color-mix(in oklab, var(--color-white) 6%, transparent)}}.sidebar-nav a.active{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-white);border-left-color:#818cf8}.sidebar-nav .nav-icon{width:calc(var(--spacing) * 4.5);opacity:.6;flex-shrink:0}.sidebar-footer{border-top-style:var(--tw-border-style);border-color:#ffffff1a;border-top-width:1px}@supports (color:color-mix(in lab, red, red)){.sidebar-footer{border-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.sidebar-footer{padding-inline:calc(var(--spacing) * 4.5);padding-block:calc(var(--spacing) * 3);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--muted-strong)}.sidebar-footer a{color:var(--border-strong);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));text-decoration-line:none}.sidebar-footer a:hover{color:var(--color-white)}.main-content{background-color:var(--canvas);min-width:0;padding-inline:calc(var(--spacing) * 8);padding-block:calc(var(--spacing) * 6);flex:1}.content-header{margin-bottom:calc(var(--spacing) * 6)}.content-header h1{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--ink)}.subtitle{margin-top:calc(var(--spacing) * .5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted)}.toolbar{margin-bottom:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);display:flex}.toolbar-spacer{flex:1}.pagination{margin-top:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);flex-wrap:wrap;display:flex}.pagination-summary{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--muted)}.pagination-summary strong{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--ink)}.pagination-controls{align-items:center;gap:calc(var(--spacing) * 1.5);flex-wrap:wrap;display:flex}.pagination-disabled{cursor:not-allowed;opacity:.4}}@layer utilities;:root{--primary:#4f46e5;--primary-hover:#4338ca;--primary-soft:#4f46e526;--canvas:#f8fafc;--surface:#fff;--surface-alt:#f1f5f9;--border:#e2e8f0;--border-strong:#94a3b8;--ink:#0f172a;--ink-soft:#1e293b;--muted:#64748b;--muted-strong:#475569;--success:#16a34a;--warning:#d97706;--danger:#dc2626;--danger-hover:#b91c1c;--info:#3b82f6;--success-bg:#dcfce7;--success-text:#15803d;--warning-bg:#fef9c3;--warning-text:#a16207;--danger-bg:#fee2e2;--danger-text:#b91c1c;--info-bg:#dbeafe;--info-text:#2563eb;--radius-sm:4px;--radius-md:6px;--radius-lg:8px;--shadow-card:0 1px 3px #0f172a0f, 0 0 0 1px #0f172a08}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@energycap/internal-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "EnergyCAP internal UI — shared Tailwind v4 theme + component classes for internal tools. Framework-agnostic.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.css",
|
|
9
|
+
"./theme": "./src/theme.css",
|
|
10
|
+
"./css": "./dist/internal-ui.css"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"dist",
|
|
15
|
+
"skills",
|
|
16
|
+
"bin",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"bin": {
|
|
20
|
+
"internal-ui": "./bin/cli.mjs"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tailwindcss -i ./src/dist-entry.css -o ./dist/internal-ui.css --minify",
|
|
24
|
+
"build:debug": "tailwindcss -i ./src/dist-entry.css -o ./dist/internal-ui.css",
|
|
25
|
+
"sync:skills": "node ./scripts/sync-skills.mjs",
|
|
26
|
+
"prepublishOnly": "npm run sync:skills && npm run build"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@tailwindcss/cli": "^4.3.0",
|
|
30
|
+
"tailwindcss": "^4.3.0"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"energycap",
|
|
37
|
+
"tailwind",
|
|
38
|
+
"tailwindcss",
|
|
39
|
+
"design-system",
|
|
40
|
+
"theme",
|
|
41
|
+
"internal"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: energycap-internal-ui
|
|
3
|
+
description: Build or restyle UI for EnergyCAP internal tools using the @energycap/internal-ui design system (a shared Tailwind v4 theme + component classes). Use when creating or editing pages, layouts, tables, forms, buttons, badges, or alerts in any internal tool (vendor-master, opus, billcapture, or a new tool) so they match the shared look and feel — Indigo/Slate palette, Inter font, and the .btn / .card / .table-container / .badge / .alert classes plus design tokens. Applies to Razor, plain HTML, React, or any frontend that consumes @energycap/internal-ui.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Building tools with @energycap/internal-ui
|
|
7
|
+
|
|
8
|
+
`@energycap/internal-ui` is the shared look-and-feel for EnergyCAP's internal
|
|
9
|
+
tools. It is a **Tailwind v4** layer — design tokens (CSS variables + utilities)
|
|
10
|
+
plus component classes composed from Tailwind utilities. You bring the markup;
|
|
11
|
+
you apply the classes. Do not reinvent buttons, cards, tables, or color choices.
|
|
12
|
+
|
|
13
|
+
## Setup (once per project)
|
|
14
|
+
|
|
15
|
+
**Tailwind v4 build** (npm + PostCSS, Vite, etc.) — preferred:
|
|
16
|
+
|
|
17
|
+
```css
|
|
18
|
+
@import "tailwindcss";
|
|
19
|
+
@import "@energycap/internal-ui";
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**No build** — link the prebuilt bundle (reset + tokens + components, no utility
|
|
23
|
+
layer):
|
|
24
|
+
|
|
25
|
+
```html
|
|
26
|
+
<link rel="stylesheet" href="node_modules/@energycap/internal-ui/dist/internal-ui.css" />
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Load Inter for the brand font (the theme expects it; it falls back to the system
|
|
30
|
+
sans otherwise):
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Rules (follow these)
|
|
37
|
+
|
|
38
|
+
1. **Use component classes first.** Reach for `.btn`, `.card`, `.table-container`,
|
|
39
|
+
`.badge`, `.alert`, `.form-control`, and the layout classes before writing
|
|
40
|
+
utility soup. They already encode the right spacing, color, and focus states.
|
|
41
|
+
2. **Never hard-code colors.** No raw hex/rgb in markup or CSS. Use the semantic
|
|
42
|
+
tokens — utilities like `text-muted`, `bg-surface`, `border-border`, or
|
|
43
|
+
`var(--primary)` in CSS. Prefer the semantic names (`primary`, `ink`, `muted`,
|
|
44
|
+
`success`) over raw Tailwind scale classes (`indigo-600`, `slate-500`).
|
|
45
|
+
3. **One page = one `.content-header h1`.** Put the page title there with an
|
|
46
|
+
optional `.subtitle`.
|
|
47
|
+
4. **Keep keyboard focus visible.** The theme provides focus rings; don't remove
|
|
48
|
+
outlines. Interactive rows use `.row-clickable` + an inner `.row-anchor`.
|
|
49
|
+
5. **Match the existing structure.** New screens should reuse the page scaffolds
|
|
50
|
+
in `reference/page-templates.md`, not invent a new layout.
|
|
51
|
+
6. **Size with Tailwind's scale.** The system is Tailwind v4 and its components
|
|
52
|
+
are composed from Tailwind's spacing/type scale. For spacing or sizing beyond
|
|
53
|
+
the component classes, use Tailwind utilities (`px-4`, `gap-2`, `max-h-[60vh]`,
|
|
54
|
+
`w-55`) — not arbitrary inline `style="…px"`.
|
|
55
|
+
|
|
56
|
+
## Token quick reference
|
|
57
|
+
|
|
58
|
+
Colors (each is a `var(--name)` and a `bg-`/`text-`/`border-` utility):
|
|
59
|
+
`primary`, `primary-hover`, `canvas` (page bg), `surface`, `surface-alt`,
|
|
60
|
+
`border`, `border-strong`, `ink` (text), `ink-soft`, `muted`, `muted-strong`,
|
|
61
|
+
`success`, `warning`, `danger`, `info`. Solid danger button hover:
|
|
62
|
+
`danger-hover`. Soft pairs for fills/text: `--success-bg`/`--success-text`
|
|
63
|
+
(and warning/danger/info).
|
|
64
|
+
Other: `--radius-sm|md|lg`, `--shadow-card`,
|
|
65
|
+
`font-sans` (Inter), `font-mono` (Cascadia Code).
|
|
66
|
+
|
|
67
|
+
## Component quick reference
|
|
68
|
+
|
|
69
|
+
| Need | Use |
|
|
70
|
+
|------|-----|
|
|
71
|
+
| Button | `.btn` + `.btn-primary` / `.btn-outline` / `.btn-danger`; `.btn-sm`, `.btn-icon-only`, `.btn-group` |
|
|
72
|
+
| Card / panel | `.card`, `.card-heading` |
|
|
73
|
+
| Data table | `<div class="table-container table-sticky"><table>…`; `.table-empty` for no-rows; `.row-clickable`+`.row-anchor` for navigable rows |
|
|
74
|
+
| Status pill | `.badge` + `.badge-success/warning/danger/info/muted` |
|
|
75
|
+
| Inline message | `.alert` + `.alert-success/warning/error/info` |
|
|
76
|
+
| Form field | `.form-group` > `label` + `.form-control` (input/select/textarea) + `.form-hint`; `.form-actions` for the button row |
|
|
77
|
+
| Page shell | `.app-shell` > `.sidebar` (+ `.sidebar-brand/-nav/-footer`, `.nav-section`, `a.active`) + `.main-content` > `.content-header` |
|
|
78
|
+
| Toolbar / pager | `.toolbar` (+ `.toolbar-spacer`), `.pagination` (+ `.pagination-summary/-controls/-disabled`) |
|
|
79
|
+
|
|
80
|
+
Full copy-paste examples for every component are in
|
|
81
|
+
`reference/components.md`. Whole-page scaffolds (list+table page, form page,
|
|
82
|
+
detail page) are in `reference/page-templates.md`. Read those before building a
|
|
83
|
+
screen.
|
|
84
|
+
|
|
85
|
+
## When something is missing
|
|
86
|
+
|
|
87
|
+
If you need a component the system doesn't have, don't hard-code a one-off —
|
|
88
|
+
that's how drift starts. Either compose it from existing tokens/classes, or add
|
|
89
|
+
it to the package properly (see the `energycap-internal-ui-contributor` skill).
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Component reference — copy-paste examples
|
|
2
|
+
|
|
3
|
+
All examples are plain HTML; apply the same classes in Razor/JSX. These mirror
|
|
4
|
+
the kitchen sink in `apps/docs/index.html`.
|
|
5
|
+
|
|
6
|
+
## Buttons
|
|
7
|
+
|
|
8
|
+
```html
|
|
9
|
+
<button class="btn btn-primary">Save</button>
|
|
10
|
+
<button class="btn btn-outline">Cancel</button>
|
|
11
|
+
<button class="btn btn-danger">Delete</button>
|
|
12
|
+
<button class="btn btn-primary btn-sm">Small</button>
|
|
13
|
+
<button class="btn btn-outline btn-icon-only" aria-label="Add">+</button>
|
|
14
|
+
|
|
15
|
+
<div class="btn-group">
|
|
16
|
+
<button class="btn btn-outline btn-sm">Prev</button>
|
|
17
|
+
<button class="btn btn-outline btn-sm">Next</button>
|
|
18
|
+
</div>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Variants: `.btn` is the base (do not use alone — always pair with a color
|
|
22
|
+
variant). `:disabled` is styled automatically.
|
|
23
|
+
|
|
24
|
+
## Cards
|
|
25
|
+
|
|
26
|
+
```html
|
|
27
|
+
<section class="card">
|
|
28
|
+
<div class="card-heading">Vendor details</div>
|
|
29
|
+
<!-- content -->
|
|
30
|
+
</section>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Tables
|
|
34
|
+
|
|
35
|
+
Wrap every `<table>` in `.table-container`. Add `.table-sticky` to keep the
|
|
36
|
+
header pinned while the body scrolls, and cap the height with a `max-h-*` utility
|
|
37
|
+
on the container so the table scrolls internally instead of growing the page.
|
|
38
|
+
|
|
39
|
+
```html
|
|
40
|
+
<div class="table-container table-sticky max-h-[60vh]">
|
|
41
|
+
<table>
|
|
42
|
+
<thead>
|
|
43
|
+
<tr><th>Vendor</th><th>Status</th><th>Matches</th></tr>
|
|
44
|
+
</thead>
|
|
45
|
+
<tbody>
|
|
46
|
+
<tr class="row-clickable">
|
|
47
|
+
<td><a class="row-anchor" href="/vendors/1">Acme Utilities</a>Acme Utilities</td>
|
|
48
|
+
<td><span class="badge badge-success">Matched</span></td>
|
|
49
|
+
<td>1,204</td>
|
|
50
|
+
</tr>
|
|
51
|
+
</tbody>
|
|
52
|
+
<tfoot>
|
|
53
|
+
<tr><td>Total</td><td></td><td>1,204</td></tr>
|
|
54
|
+
</tfoot>
|
|
55
|
+
</table>
|
|
56
|
+
</div>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Empty state:
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<tbody>
|
|
63
|
+
<tr><td class="table-empty" colspan="3">No vendors found.</td></tr>
|
|
64
|
+
</tbody>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`.row-clickable` + `.row-anchor`: the anchor fills the row for click + keyboard
|
|
68
|
+
focus without visible chrome. `.row-clickable` is `position: relative`, so the
|
|
69
|
+
absolutely-positioned `.row-anchor` resolves against the row — put the anchor in
|
|
70
|
+
the first cell and repeat the cell's text after it (the anchor's own text is
|
|
71
|
+
hidden).
|
|
72
|
+
|
|
73
|
+
## Badges
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<span class="badge badge-success">Matched</span>
|
|
77
|
+
<span class="badge badge-warning">Review</span>
|
|
78
|
+
<span class="badge badge-danger">Failed</span>
|
|
79
|
+
<span class="badge badge-info">Queued</span>
|
|
80
|
+
<span class="badge badge-muted">Draft</span>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Alerts
|
|
84
|
+
|
|
85
|
+
```html
|
|
86
|
+
<div class="alert alert-success">Saved.</div>
|
|
87
|
+
<div class="alert alert-warning">Low-confidence match — review.</div>
|
|
88
|
+
<div class="alert alert-error">Could not load records.</div>
|
|
89
|
+
<div class="alert alert-info">Sync in progress.</div>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`.alert-danger` is an alias for `.alert-error`.
|
|
93
|
+
|
|
94
|
+
## Forms
|
|
95
|
+
|
|
96
|
+
```html
|
|
97
|
+
<div class="form-group">
|
|
98
|
+
<label for="name">Vendor name</label>
|
|
99
|
+
<input id="name" class="form-control" type="text" placeholder="Acme Utilities" />
|
|
100
|
+
<p class="form-hint">As it appears on the invoice.</p>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="form-group">
|
|
104
|
+
<label for="status">Status</label>
|
|
105
|
+
<select id="status" class="form-control">
|
|
106
|
+
<option>Active</option>
|
|
107
|
+
<option>Archived</option>
|
|
108
|
+
</select>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div class="form-group">
|
|
112
|
+
<label for="notes">Notes</label>
|
|
113
|
+
<textarea id="notes" class="form-control"></textarea>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="form-actions">
|
|
117
|
+
<button class="btn btn-primary">Save</button>
|
|
118
|
+
<button class="btn btn-outline">Cancel</button>
|
|
119
|
+
</div>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`.form-control` works on `input`, `select`, and `textarea`. The select chevron
|
|
123
|
+
and textarea mono font are handled for you.
|
|
124
|
+
|
|
125
|
+
## Layout shell
|
|
126
|
+
|
|
127
|
+
```html
|
|
128
|
+
<div class="app-shell">
|
|
129
|
+
<aside class="sidebar">
|
|
130
|
+
<div class="sidebar-brand">Vendor Master</div>
|
|
131
|
+
<nav>
|
|
132
|
+
<ul class="sidebar-nav">
|
|
133
|
+
<li class="nav-section">Data</li>
|
|
134
|
+
<li><a href="/vendors" class="active">Master vendors</a></li>
|
|
135
|
+
</ul>
|
|
136
|
+
</nav>
|
|
137
|
+
<div class="sidebar-footer">Signed in as you</div>
|
|
138
|
+
</aside>
|
|
139
|
+
|
|
140
|
+
<main class="main-content">
|
|
141
|
+
<header class="content-header">
|
|
142
|
+
<h1>Master vendors</h1>
|
|
143
|
+
<p class="subtitle">All vendors across the database.</p>
|
|
144
|
+
</header>
|
|
145
|
+
<!-- page content -->
|
|
146
|
+
</main>
|
|
147
|
+
</div>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Toolbar + pagination
|
|
151
|
+
|
|
152
|
+
```html
|
|
153
|
+
<div class="toolbar">
|
|
154
|
+
<h2 class="text-base font-semibold text-ink">Master vendors</h2>
|
|
155
|
+
<button class="btn btn-primary btn-sm">Add vendor</button>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div class="pagination">
|
|
159
|
+
<span class="pagination-summary"><strong>1–50</strong> of 1,204</span>
|
|
160
|
+
<div class="pagination-controls">
|
|
161
|
+
<a class="btn btn-outline btn-icon-only" href="?page=1" aria-label="Previous">‹</a>
|
|
162
|
+
<span class="text-sm text-muted">Page 1 of 25</span>
|
|
163
|
+
<a class="btn btn-outline btn-icon-only" href="?page=2" aria-label="Next">›</a>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
```
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Page templates
|
|
2
|
+
|
|
3
|
+
Full-page scaffolds. Start from these so every screen has the same skeleton:
|
|
4
|
+
`.app-shell` > `.sidebar` + `.main-content` > `.content-header` + content.
|
|
5
|
+
|
|
6
|
+
## List page with searchable, sortable table
|
|
7
|
+
|
|
8
|
+
The most common internal-tool screen. Search toolbar, sticky-header table, pager.
|
|
9
|
+
|
|
10
|
+
```html
|
|
11
|
+
<div class="app-shell">
|
|
12
|
+
<aside class="sidebar">
|
|
13
|
+
<div class="sidebar-brand">Vendor Master</div>
|
|
14
|
+
<nav>
|
|
15
|
+
<ul class="sidebar-nav">
|
|
16
|
+
<li class="nav-section">Data</li>
|
|
17
|
+
<li><a href="/vendors" class="active">Master vendors</a></li>
|
|
18
|
+
</ul>
|
|
19
|
+
</nav>
|
|
20
|
+
</aside>
|
|
21
|
+
|
|
22
|
+
<main class="main-content">
|
|
23
|
+
<header class="content-header">
|
|
24
|
+
<h1>Master vendors</h1>
|
|
25
|
+
<p class="subtitle">All vendors across the database.</p>
|
|
26
|
+
</header>
|
|
27
|
+
|
|
28
|
+
<div class="toolbar">
|
|
29
|
+
<form method="get" role="search" class="grow">
|
|
30
|
+
<input class="form-control max-w-md" type="search" name="q" placeholder="Search vendors…" />
|
|
31
|
+
</form>
|
|
32
|
+
<button class="btn btn-primary btn-sm">Add vendor</button>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div class="table-container table-sticky max-h-[60vh]">
|
|
36
|
+
<table>
|
|
37
|
+
<thead>
|
|
38
|
+
<tr><th>Vendor</th><th>Status</th><th>Matches</th></tr>
|
|
39
|
+
</thead>
|
|
40
|
+
<tbody>
|
|
41
|
+
<!-- rows; use .row-clickable + .row-anchor when rows navigate -->
|
|
42
|
+
<tr><td class="table-empty" colspan="3">No vendors found.</td></tr>
|
|
43
|
+
</tbody>
|
|
44
|
+
</table>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="pagination">
|
|
48
|
+
<span class="pagination-summary"><strong>0</strong> results</span>
|
|
49
|
+
<div class="pagination-controls">…</div>
|
|
50
|
+
</div>
|
|
51
|
+
</main>
|
|
52
|
+
</div>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Form page
|
|
56
|
+
|
|
57
|
+
```html
|
|
58
|
+
<main class="main-content">
|
|
59
|
+
<header class="content-header">
|
|
60
|
+
<h1>Edit vendor</h1>
|
|
61
|
+
</header>
|
|
62
|
+
|
|
63
|
+
<form>
|
|
64
|
+
<section class="card max-w-xl">
|
|
65
|
+
<div class="card-heading">Details</div>
|
|
66
|
+
|
|
67
|
+
<div class="form-group">
|
|
68
|
+
<label for="name">Name</label>
|
|
69
|
+
<input id="name" class="form-control" type="text" />
|
|
70
|
+
<p class="form-hint">Required.</p>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div class="form-actions">
|
|
74
|
+
<button class="btn btn-primary" type="submit">Save</button>
|
|
75
|
+
<a class="btn btn-outline" href="/vendors">Cancel</a>
|
|
76
|
+
</div>
|
|
77
|
+
</section>
|
|
78
|
+
</form>
|
|
79
|
+
</main>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Detail page (two-column)
|
|
83
|
+
|
|
84
|
+
```html
|
|
85
|
+
<main class="main-content">
|
|
86
|
+
<header class="content-header">
|
|
87
|
+
<h1>Acme Utilities</h1>
|
|
88
|
+
<p class="subtitle">Master vendor · 1,204 matches</p>
|
|
89
|
+
</header>
|
|
90
|
+
|
|
91
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
|
|
92
|
+
<section class="card">
|
|
93
|
+
<div class="card-heading">Summary</div>
|
|
94
|
+
<!-- key/value details -->
|
|
95
|
+
</section>
|
|
96
|
+
<section class="card">
|
|
97
|
+
<div class="card-heading">Recent activity</div>
|
|
98
|
+
<!-- table or list -->
|
|
99
|
+
</section>
|
|
100
|
+
</div>
|
|
101
|
+
</main>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The `grid-cols-1 lg:grid-cols-2` utilities already collapse the layout to one
|
|
105
|
+
column on narrow viewports.
|
package/src/base.css
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
2
|
+
Base element styling — layered on top of Tailwind's preflight.
|
|
3
|
+
Kept intentionally minimal and complementary: preflight already resets
|
|
4
|
+
box-sizing, margins, borders, etc. We only add the brand defaults
|
|
5
|
+
(body font/color/background), focus visibility, and a couple of element
|
|
6
|
+
tweaks preflight strips.
|
|
7
|
+
════════════════════════════════════════════════════════════════════ */
|
|
8
|
+
|
|
9
|
+
@layer base {
|
|
10
|
+
body {
|
|
11
|
+
font-family: var(--font-sans, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
|
12
|
+
color: var(--ink);
|
|
13
|
+
background: var(--canvas);
|
|
14
|
+
line-height: 1.5;
|
|
15
|
+
-webkit-font-smoothing: antialiased;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Preflight removes list styling; restore it for lists in body content.
|
|
19
|
+
(Nav lists opt back out with the `list-none` utility.) */
|
|
20
|
+
ol {
|
|
21
|
+
list-style: decimal;
|
|
22
|
+
padding-left: 1.5em;
|
|
23
|
+
}
|
|
24
|
+
ul {
|
|
25
|
+
list-style: disc;
|
|
26
|
+
padding-left: 1.5em;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Keyboard focus ring — visible for keyboard users, suppressed for pointer. */
|
|
30
|
+
*:focus {
|
|
31
|
+
outline: none;
|
|
32
|
+
}
|
|
33
|
+
*:focus-visible {
|
|
34
|
+
outline: 2px solid var(--primary);
|
|
35
|
+
outline-offset: 2px;
|
|
36
|
+
border-radius: 3px;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.alert {
|
|
3
|
+
@apply flex items-center gap-2 px-3.5 py-2.5 mb-4 rounded-md text-sm font-medium;
|
|
4
|
+
}
|
|
5
|
+
.alert-success { @apply border border-success; background: var(--success-bg); color: var(--success-text); }
|
|
6
|
+
.alert-warning { @apply border border-warning; background: var(--warning-bg); color: var(--warning-text); }
|
|
7
|
+
.alert-error,
|
|
8
|
+
.alert-danger { @apply border border-danger; background: var(--danger-bg); color: var(--danger-text); }
|
|
9
|
+
.alert-info { @apply border border-info; background: var(--info-bg); color: var(--info-text); }
|
|
10
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.badge {
|
|
3
|
+
@apply inline-flex items-center gap-1 px-2 py-0.5 rounded-full
|
|
4
|
+
text-xs font-semibold whitespace-nowrap;
|
|
5
|
+
}
|
|
6
|
+
.badge-success { background: var(--success-bg); color: var(--success-text); }
|
|
7
|
+
.badge-warning { background: var(--warning-bg); color: var(--warning-text); }
|
|
8
|
+
.badge-danger { background: var(--danger-bg); color: var(--danger-text); }
|
|
9
|
+
.badge-info { background: var(--info-bg); color: var(--info-text); }
|
|
10
|
+
.badge-muted { @apply bg-surface-alt text-muted-strong; }
|
|
11
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.btn {
|
|
3
|
+
@apply inline-flex items-center justify-center gap-1.5 px-3.5 py-1.5
|
|
4
|
+
rounded-md text-sm font-medium leading-normal text-white
|
|
5
|
+
no-underline cursor-pointer transition-colors;
|
|
6
|
+
}
|
|
7
|
+
.btn:hover { @apply text-white no-underline; }
|
|
8
|
+
.btn:disabled { @apply opacity-60 cursor-not-allowed; }
|
|
9
|
+
.btn:focus-visible {
|
|
10
|
+
@apply outline-none;
|
|
11
|
+
box-shadow: 0 0 0 3px var(--primary-soft);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.btn-primary { @apply bg-primary; }
|
|
15
|
+
.btn-primary:hover { @apply bg-primary-hover; }
|
|
16
|
+
|
|
17
|
+
.btn-danger { @apply bg-danger; }
|
|
18
|
+
.btn-danger:hover { @apply bg-danger-hover; }
|
|
19
|
+
|
|
20
|
+
.btn-outline { @apply bg-surface border border-border text-ink; }
|
|
21
|
+
.btn-outline:hover { @apply bg-canvas border-border-strong text-ink; }
|
|
22
|
+
|
|
23
|
+
.btn-sm { @apply px-2.5 py-1 text-xs; }
|
|
24
|
+
.btn-icon-only { @apply w-8 h-8 p-0; }
|
|
25
|
+
.btn-group { @apply inline-flex items-center gap-1.5; }
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.card {
|
|
3
|
+
@apply px-6 py-5 bg-surface border border-border rounded-lg;
|
|
4
|
+
box-shadow: var(--shadow-card);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.card-heading {
|
|
8
|
+
@apply flex items-center gap-2 mb-4 pb-2.5 border-b border-border
|
|
9
|
+
text-sm font-semibold text-ink-soft;
|
|
10
|
+
}
|
|
11
|
+
.card-heading i { @apply text-sm text-muted; }
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.form-group { @apply mb-5; }
|
|
3
|
+
.form-group > label { @apply block mb-1.5 text-sm font-semibold text-ink; }
|
|
4
|
+
|
|
5
|
+
.form-control {
|
|
6
|
+
@apply w-full px-3 py-2 bg-surface border border-border rounded-md
|
|
7
|
+
text-ink text-sm transition-colors;
|
|
8
|
+
font-family: inherit;
|
|
9
|
+
}
|
|
10
|
+
.form-control::placeholder { @apply text-border-strong; }
|
|
11
|
+
.form-control:focus {
|
|
12
|
+
@apply outline-none border-primary;
|
|
13
|
+
box-shadow: 0 0 0 3px var(--primary-soft);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
textarea.form-control { @apply min-h-30 resize-y font-mono text-sm leading-relaxed; }
|
|
17
|
+
|
|
18
|
+
select.form-control {
|
|
19
|
+
@apply appearance-none cursor-pointer pr-9 leading-snug;
|
|
20
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='7'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%2394a3b8' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
21
|
+
background-repeat: no-repeat;
|
|
22
|
+
background-position: right 12px center;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.form-hint { @apply mt-1 text-xs text-muted; }
|
|
26
|
+
|
|
27
|
+
.form-actions { @apply flex gap-2.5 mt-6 pt-4 border-t border-border; }
|
|
28
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
/* App shell: sidebar + content side by side, full viewport height. */
|
|
3
|
+
.app-shell { @apply flex min-h-dvh; }
|
|
4
|
+
|
|
5
|
+
/* ── Sidebar ──────────────────────────────────────────────────── */
|
|
6
|
+
.sidebar { @apply flex flex-col shrink-0 w-55 bg-ink; }
|
|
7
|
+
.sidebar-brand {
|
|
8
|
+
@apply flex items-center gap-2.5 px-4.5 py-4 text-sm font-bold tracking-wide
|
|
9
|
+
text-white border-b border-white/10;
|
|
10
|
+
}
|
|
11
|
+
/* px-0 (alongside m-0) keeps the nav flush even under a consumer global
|
|
12
|
+
`ul { padding-left }` reset — list padding must never indent nav items. */
|
|
13
|
+
.sidebar-nav { @apply flex-1 m-0 px-0 py-2 list-none overflow-y-auto; }
|
|
14
|
+
.sidebar-nav .nav-section {
|
|
15
|
+
@apply px-4.5 pt-4 pb-1.5 text-xs font-semibold uppercase tracking-wider text-muted-strong;
|
|
16
|
+
}
|
|
17
|
+
.sidebar-nav a {
|
|
18
|
+
@apply flex items-center gap-2.5 px-4.5 py-2 text-sm font-medium
|
|
19
|
+
text-border-strong no-underline border-l-[3px] border-transparent transition-colors;
|
|
20
|
+
}
|
|
21
|
+
.sidebar-nav a:hover { @apply bg-white/[0.06] text-white; }
|
|
22
|
+
.sidebar-nav a.active {
|
|
23
|
+
@apply bg-white/[0.06] text-white font-semibold;
|
|
24
|
+
border-left-color: #818cf8; /* indigo-400 */
|
|
25
|
+
}
|
|
26
|
+
.sidebar-nav .nav-icon { @apply w-4.5 shrink-0 opacity-60; }
|
|
27
|
+
.sidebar-footer {
|
|
28
|
+
@apply px-4.5 py-3 text-xs text-muted-strong border-t border-white/10;
|
|
29
|
+
}
|
|
30
|
+
.sidebar-footer a { @apply text-border-strong no-underline transition-colors; }
|
|
31
|
+
.sidebar-footer a:hover { @apply text-white; }
|
|
32
|
+
|
|
33
|
+
/* ── Main content ─────────────────────────────────────────────── */
|
|
34
|
+
.main-content { @apply flex-1 min-w-0 px-8 py-6 bg-canvas; }
|
|
35
|
+
.content-header { @apply mb-6; }
|
|
36
|
+
.content-header h1 { @apply text-xl font-semibold text-ink; }
|
|
37
|
+
.subtitle { @apply mt-0.5 text-sm text-muted; }
|
|
38
|
+
|
|
39
|
+
/* ── Toolbar ──────────────────────────────────────────────────── */
|
|
40
|
+
.toolbar { @apply flex items-center justify-between gap-3 mb-4; }
|
|
41
|
+
.toolbar-spacer { @apply flex-1; }
|
|
42
|
+
|
|
43
|
+
/* ── Pagination ───────────────────────────────────────────────── */
|
|
44
|
+
.pagination { @apply flex items-center justify-between gap-3 mt-4 flex-wrap; }
|
|
45
|
+
.pagination-summary { @apply text-sm text-muted; }
|
|
46
|
+
.pagination-summary strong { @apply text-ink font-semibold; }
|
|
47
|
+
.pagination-controls { @apply flex items-center gap-1.5 flex-wrap; }
|
|
48
|
+
.pagination-disabled { @apply opacity-40 cursor-not-allowed; }
|
|
49
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
/* Wrap a <table> in .table-container to get the framed, scrollable card and
|
|
3
|
+
consistent header/cell styling. Styling is scoped to the container so bare
|
|
4
|
+
<table> elements elsewhere are left untouched. */
|
|
5
|
+
.table-container {
|
|
6
|
+
@apply max-w-full overflow-auto bg-surface border border-border rounded-lg;
|
|
7
|
+
box-shadow: var(--shadow-card);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.table-container table { @apply w-full border-collapse; }
|
|
11
|
+
|
|
12
|
+
.table-container thead th {
|
|
13
|
+
@apply px-4 py-2.5 text-left text-xs font-semibold uppercase tracking-wide
|
|
14
|
+
text-muted bg-surface-alt border-b border-border;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.table-container tbody td {
|
|
18
|
+
@apply px-4 py-2.5 text-sm align-middle border-b border-border;
|
|
19
|
+
}
|
|
20
|
+
.table-container tbody tr:last-child td { @apply border-b-0; }
|
|
21
|
+
.table-container tbody tr:hover { @apply bg-canvas; }
|
|
22
|
+
|
|
23
|
+
.table-container tfoot td {
|
|
24
|
+
@apply px-4 py-2.5 text-sm font-semibold text-ink-soft bg-canvas border-t-2 border-border;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.table-empty { @apply px-4 py-10 text-center text-muted; }
|
|
28
|
+
|
|
29
|
+
/* Sticky header — add `table-sticky` alongside `table-container`. */
|
|
30
|
+
.table-sticky thead th {
|
|
31
|
+
@apply sticky top-0 z-[2] bg-surface-alt;
|
|
32
|
+
/* box-shadow rather than border-b: collapsed borders drop on sticky thead. */
|
|
33
|
+
box-shadow: inset 0 -1px 0 var(--border);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Clickable rows: the whole <tr> navigates on click, while an inner
|
|
37
|
+
<a class="row-anchor"> gives keyboard focus + an assistive-tech anchor
|
|
38
|
+
without rendering any visible chrome. */
|
|
39
|
+
.row-clickable { @apply relative cursor-pointer; }
|
|
40
|
+
.row-clickable:focus-within { @apply bg-canvas; }
|
|
41
|
+
.row-anchor {
|
|
42
|
+
@apply absolute inset-0 z-[1] overflow-hidden;
|
|
43
|
+
text-indent: -9999px;
|
|
44
|
+
color: inherit;
|
|
45
|
+
}
|
|
46
|
+
.row-anchor:focus-visible {
|
|
47
|
+
@apply outline-2 outline-primary rounded-[2px];
|
|
48
|
+
outline-offset: -2px;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/* Entry used ONLY to compile the standalone prebuilt bundle
|
|
2
|
+
(dist/internal-ui.css) via `@tailwindcss/cli`. It pulls in Tailwind (for the
|
|
3
|
+
preflight reset + layer ordering + token machinery) and then the package.
|
|
4
|
+
|
|
5
|
+
No-build consumers link the compiled dist; Tailwind-build consumers import
|
|
6
|
+
the package's "." export (src/index.css) after their own `@import
|
|
7
|
+
"tailwindcss"` instead. */
|
|
8
|
+
|
|
9
|
+
/* source(none): the prebuilt bundle is deterministic — reset + tokens +
|
|
10
|
+
component classes only, no utilities harvested from whatever files happen to
|
|
11
|
+
sit nearby. Build consumers get utilities from their own content scan. */
|
|
12
|
+
@import "tailwindcss" source(none);
|
|
13
|
+
@import "./index.css";
|
package/src/index.css
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* @energycap/internal-ui — package entry.
|
|
2
|
+
Import AFTER `@import "tailwindcss"` in a Tailwind v4 build:
|
|
3
|
+
|
|
4
|
+
@import "tailwindcss";
|
|
5
|
+
@import "@energycap/internal-ui";
|
|
6
|
+
|
|
7
|
+
Order matters: tokens first (so utilities + component CSS can resolve them),
|
|
8
|
+
then base, then the component layer. */
|
|
9
|
+
|
|
10
|
+
@import "./theme.css";
|
|
11
|
+
@import "./base.css";
|
|
12
|
+
|
|
13
|
+
@import "./components/buttons.css";
|
|
14
|
+
@import "./components/forms.css";
|
|
15
|
+
@import "./components/cards.css";
|
|
16
|
+
@import "./components/tables.css";
|
|
17
|
+
@import "./components/badges.css";
|
|
18
|
+
@import "./components/alerts.css";
|
|
19
|
+
@import "./components/layout.css";
|
package/src/theme.css
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
2
|
+
@energycap/internal-ui — design tokens (single source of truth)
|
|
3
|
+
--------------------------------------------------------------------
|
|
4
|
+
Formalizes the palette the EnergyCAP internal tools already converged
|
|
5
|
+
on: Indigo primary, Slate neutrals, Inter / Cascadia Code. Values mirror
|
|
6
|
+
the proven tokens shipped in Opus so existing screens shift minimally.
|
|
7
|
+
|
|
8
|
+
Pattern (Tailwind v4): the raw tokens live in :root so they are ALWAYS
|
|
9
|
+
emitted — component CSS can rely on `var(--token)` resolving regardless of
|
|
10
|
+
which utilities a consumer happens to generate. `@theme inline` then re-
|
|
11
|
+
exposes them as Tailwind utilities (bg-primary, text-muted, …) that point
|
|
12
|
+
straight back at the :root vars, with no duplicate variables.
|
|
13
|
+
════════════════════════════════════════════════════════════════════ */
|
|
14
|
+
|
|
15
|
+
:root {
|
|
16
|
+
/* Brand / actions */
|
|
17
|
+
--primary: #4f46e5; /* indigo-600 */
|
|
18
|
+
--primary-hover: #4338ca; /* indigo-700 */
|
|
19
|
+
--primary-soft: rgb(79 70 229 / 0.15); /* indigo-600 @ 15% — focus ring / soft fill */
|
|
20
|
+
|
|
21
|
+
/* Surfaces */
|
|
22
|
+
--canvas: #f8fafc; /* slate-50 — app background */
|
|
23
|
+
--surface: #ffffff; /* cards, inputs, panels */
|
|
24
|
+
--surface-alt: #f1f5f9; /* slate-100 — table headers, subtle fills */
|
|
25
|
+
|
|
26
|
+
/* Lines */
|
|
27
|
+
--border: #e2e8f0; /* slate-200 */
|
|
28
|
+
--border-strong: #94a3b8; /* slate-400 */
|
|
29
|
+
|
|
30
|
+
/* Text */
|
|
31
|
+
--ink: #0f172a; /* slate-900 — primary text */
|
|
32
|
+
--ink-soft: #1e293b; /* slate-800 */
|
|
33
|
+
--muted: #64748b; /* slate-500 */
|
|
34
|
+
--muted-strong: #475569; /* slate-600 */
|
|
35
|
+
|
|
36
|
+
/* Semantic — solid (borders, icons, fills) */
|
|
37
|
+
--success: #16a34a; /* green-600 */
|
|
38
|
+
--warning: #d97706; /* amber-600 */
|
|
39
|
+
--danger: #dc2626; /* red-600 */
|
|
40
|
+
--danger-hover: #b91c1c; /* red-700 — solid danger button hover */
|
|
41
|
+
--info: #3b82f6; /* blue-500 */
|
|
42
|
+
|
|
43
|
+
/* Semantic — soft background + readable text (alerts, badges) */
|
|
44
|
+
--success-bg: #dcfce7; --success-text: #15803d; /* green-100 / green-700 */
|
|
45
|
+
--warning-bg: #fef9c3; --warning-text: #a16207; /* yellow-100 / yellow-700 */
|
|
46
|
+
--danger-bg: #fee2e2; --danger-text: #b91c1c; /* red-100 / red-700 */
|
|
47
|
+
--info-bg: #dbeafe; --info-text: #2563eb; /* blue-100 / blue-600 */
|
|
48
|
+
|
|
49
|
+
/* Radius (matches Tailwind v4 defaults; redeclared so component CSS resolves) */
|
|
50
|
+
--radius-sm: 4px;
|
|
51
|
+
--radius-md: 6px;
|
|
52
|
+
--radius-lg: 8px;
|
|
53
|
+
|
|
54
|
+
/* Elevation (custom name so Tailwind's own shadow-sm/md stay untouched) */
|
|
55
|
+
--shadow-card: 0 1px 3px rgb(15 23 42 / 0.06), 0 0 0 1px rgb(15 23 42 / 0.03);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Typography — override Tailwind's default font stacks. */
|
|
59
|
+
@theme {
|
|
60
|
+
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
61
|
+
--font-mono: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Expose the semantic colors as Tailwind utilities (bg-primary, text-ink,
|
|
65
|
+
border-border, text-success, …) referencing the :root vars directly. */
|
|
66
|
+
@theme inline {
|
|
67
|
+
--color-primary: var(--primary);
|
|
68
|
+
--color-primary-hover: var(--primary-hover);
|
|
69
|
+
--color-canvas: var(--canvas);
|
|
70
|
+
--color-surface: var(--surface);
|
|
71
|
+
--color-surface-alt: var(--surface-alt);
|
|
72
|
+
--color-border: var(--border);
|
|
73
|
+
--color-border-strong: var(--border-strong);
|
|
74
|
+
--color-ink: var(--ink);
|
|
75
|
+
--color-ink-soft: var(--ink-soft);
|
|
76
|
+
--color-muted: var(--muted);
|
|
77
|
+
--color-muted-strong: var(--muted-strong);
|
|
78
|
+
--color-success: var(--success);
|
|
79
|
+
--color-warning: var(--warning);
|
|
80
|
+
--color-danger: var(--danger);
|
|
81
|
+
--color-danger-hover: var(--danger-hover);
|
|
82
|
+
--color-info: var(--info);
|
|
83
|
+
}
|