@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 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
+ }