@filipgutica/ui 1.3.0 → 1.4.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 CHANGED
@@ -87,6 +87,48 @@ Themes supply colors; the library keeps typography, spacing, and component
87
87
  structure consistent. Open VSX search and imports are available through
88
88
  `@filipgutica/ui/open-vsx`, which must run on a Node server.
89
89
 
90
+ The default theme is unchanged. To opt in to Graphite Indigo, import it after
91
+ `theme.css`:
92
+
93
+ ```css
94
+ @import "@filipgutica/ui/theme.css";
95
+ @import "@filipgutica/ui/themes/graphite-indigo.css";
96
+ ```
97
+
98
+ ## Project sites
99
+
100
+ The `site` entry gives the project sites one header, one appearance choice, and
101
+ shared page styles. Import `@filipgutica/ui/site.css` and the Graphite Indigo
102
+ theme above, which supplies the type and spacing roles `site.css` reads. Keep
103
+ document-wide link styles in `@layer base` so the header owns its navigation
104
+ appearance.
105
+
106
+ ```vue
107
+ <script setup lang="ts">
108
+ import { UiSiteHeader } from '@filipgutica/ui/site'
109
+ </script>
110
+
111
+ <template>
112
+ <UiSiteHeader
113
+ project="wtree"
114
+ :links="[{ label: 'User guide', href: '#guide' }]"
115
+ />
116
+ </template>
117
+ ```
118
+
119
+ `project` is `wtree`, `annoterm`, `devps`, `workbench`, or `ui`. The project
120
+ menu is a native disclosure that works without JavaScript; Escape closes it and
121
+ returns focus to its button. Without a default slot the header renders a
122
+ System, Light, and Dark picker. A page with its own appearance control passes it
123
+ in the default slot and uses `useSiteAppearance({ applyToRoot: false })` when it
124
+ owns the root element.
125
+
126
+ `useSiteAppearance()` returns `choice`, the resolved `appearance`, and
127
+ `chooseAppearance(value)`. The choice is stored under `tool-site-theme`, follows
128
+ the operating system and other tabs, and is safe during server rendering and
129
+ when storage is blocked. To avoid a flash of the wrong appearance, inline
130
+ `siteThemeBootScript` in the document head.
131
+
90
132
  ## Development
91
133
 
92
134
  ```sh
@@ -94,3 +136,8 @@ pnpm install
94
136
  pnpm docs:dev
95
137
  pnpm verify
96
138
  ```
139
+
140
+ Graphite Indigo values live in `themes/graphite-indigo.json`. After editing it,
141
+ run `pnpm themes:build` to regenerate the opt-in theme and the standalone
142
+ `themes/graphite-indigo.tokens.css`; `pnpm themes:check` fails when either is
143
+ stale.
@@ -0,0 +1,24 @@
1
+ import type { SiteProjectId } from './projects.js';
2
+ export interface UiSiteLink {
3
+ label: string;
4
+ href: string;
5
+ }
6
+ export interface UiSiteHeaderProps {
7
+ /** The site this header belongs to. It is marked as current in the project menu. */
8
+ project: SiteProjectId;
9
+ /** Page links shown beside the appearance control, such as a user guide or releases. */
10
+ links?: readonly UiSiteLink[];
11
+ }
12
+ type __VLS_Slots = {
13
+ /** Replaces the System, Light, and Dark picker with a page-specific appearance control. */
14
+ default?(): unknown;
15
+ };
16
+ declare const __VLS_base: import("vue").DefineComponent<UiSiteHeaderProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<UiSiteHeaderProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
17
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
18
+ declare const _default: typeof __VLS_export;
19
+ export default _default;
20
+ type __VLS_WithSlots<T, S> = T & {
21
+ new (): {
22
+ $slots: S;
23
+ };
24
+ };
@@ -0,0 +1,35 @@
1
+ import type { ComputedRef } from 'vue';
2
+ export type SiteAppearanceChoice = 'system' | 'light' | 'dark';
3
+ export type SiteAppearance = 'light' | 'dark';
4
+ /** The choice is shared by every site in the family, so it must not change. */
5
+ export declare const SITE_APPEARANCE_KEY = "tool-site-theme";
6
+ /**
7
+ * Inline this in the document head, before the stylesheet paints, to avoid a flash
8
+ * of the wrong appearance. It applies the stored choice exactly as
9
+ * `applySiteAppearance` does and never throws.
10
+ */
11
+ export declare const siteThemeBootScript = "(function () {\n var root = document.documentElement, choice;\n try { choice = localStorage.getItem('tool-site-theme'); } catch (error) {}\n var dark = choice === 'dark';\n if (choice !== 'light' && choice !== 'dark') {\n try { dark = matchMedia('(prefers-color-scheme: dark)').matches; } catch (error) {}\n }\n root.classList.toggle('dark', dark);\n if (choice === 'light' || choice === 'dark') root.dataset.theme = choice;\n})();";
12
+ /** System removes the explicit override. Light and dark set it. */
13
+ export declare const applySiteAppearance: ({ root, choice, appearance }: {
14
+ root: HTMLElement;
15
+ choice: SiteAppearanceChoice;
16
+ appearance: SiteAppearance;
17
+ }) => void;
18
+ /**
19
+ * The shared System, Light, or Dark choice with the appearance it resolves to.
20
+ * Every call shares the stored choice, and other tabs and the operating system
21
+ * update it. Nothing reads storage or the browser until the component mounts, so
22
+ * server rendering and hydration see `system`. Storage failures leave the choice
23
+ * working for the current visit.
24
+ *
25
+ * By default the composable applies the appearance to `<html>`. Pass
26
+ * `applyToRoot: false` when the page owns the root, for example while an imported
27
+ * theme is active, and call `applySiteAppearance` yourself.
28
+ */
29
+ export declare const useSiteAppearance: ({ applyToRoot }?: {
30
+ applyToRoot?: boolean;
31
+ }) => {
32
+ choice: ComputedRef<SiteAppearanceChoice>;
33
+ appearance: ComputedRef<SiteAppearance>;
34
+ chooseAppearance: (value: SiteAppearanceChoice) => void;
35
+ };
@@ -0,0 +1,6 @@
1
+ export { default as UiSiteHeader } from './UiSiteHeader.vue';
2
+ export type { UiSiteHeaderProps, UiSiteLink } from './UiSiteHeader.vue';
3
+ export { SITE_PROJECTS } from './projects.js';
4
+ export type { SiteProject, SiteProjectId } from './projects.js';
5
+ export { applySiteAppearance, SITE_APPEARANCE_KEY, siteThemeBootScript, useSiteAppearance, } from './appearance.js';
6
+ export type { SiteAppearance, SiteAppearanceChoice } from './appearance.js';
@@ -0,0 +1,36 @@
1
+ declare const projects: readonly [{
2
+ readonly id: "annoterm";
3
+ readonly label: "annoterm";
4
+ readonly href: "https://filipgutica.github.io/annoterm/";
5
+ readonly repository: "https://github.com/filipgutica/annoterm";
6
+ }, {
7
+ readonly id: "wtree";
8
+ readonly label: "wtree";
9
+ readonly href: "https://filipgutica.github.io/wtree/";
10
+ readonly repository: "https://github.com/filipgutica/wtree";
11
+ }, {
12
+ readonly id: "devps";
13
+ readonly label: "devps";
14
+ readonly href: "https://filipgutica.github.io/devps/";
15
+ readonly repository: "https://github.com/filipgutica/devps";
16
+ }, {
17
+ readonly id: "workbench";
18
+ readonly label: "Workbench";
19
+ readonly href: "https://filipgutica.github.io/t3code/";
20
+ readonly repository: "https://github.com/filipgutica/t3code";
21
+ }, {
22
+ readonly id: "ui";
23
+ readonly label: "UI";
24
+ readonly href: "https://filipgutica.github.io/ui/";
25
+ readonly repository: "https://github.com/filipgutica/ui";
26
+ }];
27
+ export type SiteProjectId = (typeof projects)[number]['id'];
28
+ export interface SiteProject {
29
+ readonly id: SiteProjectId;
30
+ readonly label: string;
31
+ readonly href: string;
32
+ readonly repository: string;
33
+ }
34
+ /** Every site in the family, in the order the project menu lists them. */
35
+ export declare const SITE_PROJECTS: readonly SiteProject[];
36
+ export {};
package/dist/site.js ADDED
@@ -0,0 +1,177 @@
1
+ import { Fragment as e, computed as t, createCommentVNode as n, createElementBlock as r, createElementVNode as i, defineComponent as a, onBeforeUnmount as o, onMounted as s, openBlock as c, ref as l, renderList as u, renderSlot as d, toDisplayString as f, unref as p, useSlots as m, watch as h } from "vue";
2
+ import { tryOnMounted as g, usePreferredDark as _, useStorage as v } from "@vueuse/core";
3
+ //#region src/site/appearance.ts
4
+ var y = "tool-site-theme", b = (e) => e === "system" || e === "light" || e === "dark", x = `(function () {
5
+ var root = document.documentElement, choice;
6
+ try { choice = localStorage.getItem('${y}'); } catch (error) {}
7
+ var dark = choice === 'dark';
8
+ if (choice !== 'light' && choice !== 'dark') {
9
+ try { dark = matchMedia('(prefers-color-scheme: dark)').matches; } catch (error) {}
10
+ }
11
+ root.classList.toggle('dark', dark);
12
+ if (choice === 'light' || choice === 'dark') root.dataset.theme = choice;
13
+ })();`, S = ({ root: e, choice: t, appearance: n }) => {
14
+ e.classList.toggle("dark", n === "dark"), t === "system" ? delete e.dataset.theme : e.dataset.theme = t;
15
+ }, C = ({ applyToRoot: e = !0 } = {}) => {
16
+ let n = v(y, "system", void 0, {
17
+ initOnMounted: !0,
18
+ writeDefaults: !1,
19
+ onError: () => {},
20
+ serializer: {
21
+ read: (e) => b(e) ? e : "system",
22
+ write: (e) => e
23
+ }
24
+ }), r = _(), i = t(() => n.value), a = t(() => n.value === "system" ? r.value ? "dark" : "light" : n.value);
25
+ return e && g(() => {
26
+ h([i, a], () => S({
27
+ root: document.documentElement,
28
+ choice: i.value,
29
+ appearance: a.value
30
+ }), { immediate: !0 });
31
+ }), {
32
+ choice: i,
33
+ appearance: a,
34
+ chooseAppearance: (e) => {
35
+ b(e) && (n.value = e);
36
+ }
37
+ };
38
+ }, w = [
39
+ {
40
+ id: "annoterm",
41
+ label: "annoterm",
42
+ href: "https://filipgutica.github.io/annoterm/",
43
+ repository: "https://github.com/filipgutica/annoterm"
44
+ },
45
+ {
46
+ id: "wtree",
47
+ label: "wtree",
48
+ href: "https://filipgutica.github.io/wtree/",
49
+ repository: "https://github.com/filipgutica/wtree"
50
+ },
51
+ {
52
+ id: "devps",
53
+ label: "devps",
54
+ href: "https://filipgutica.github.io/devps/",
55
+ repository: "https://github.com/filipgutica/devps"
56
+ },
57
+ {
58
+ id: "workbench",
59
+ label: "Workbench",
60
+ href: "https://filipgutica.github.io/t3code/",
61
+ repository: "https://github.com/filipgutica/t3code"
62
+ },
63
+ {
64
+ id: "ui",
65
+ label: "UI",
66
+ href: "https://filipgutica.github.io/ui/",
67
+ repository: "https://github.com/filipgutica/ui"
68
+ }
69
+ ], T = { class: "fg-site-header" }, E = { class: "fg-site-header__inner" }, D = ["aria-label"], O = {
70
+ class: "fg-site-projects__menu fg-site-nav",
71
+ "aria-label": "Projects"
72
+ }, k = ["href", "aria-current"], A = { class: "fg-site-header__end" }, j = {
73
+ key: 0,
74
+ class: "fg-site-header__links",
75
+ "aria-label": "Site links"
76
+ }, M = ["href"], N = {
77
+ key: 0,
78
+ class: "fg-site-appearance",
79
+ role: "group",
80
+ "aria-label": "Color theme"
81
+ }, P = [
82
+ "aria-label",
83
+ "title",
84
+ "aria-pressed",
85
+ "onClick"
86
+ ], F = {
87
+ viewBox: "0 0 24 24",
88
+ width: "18",
89
+ height: "18",
90
+ fill: "none",
91
+ stroke: "currentColor",
92
+ "stroke-width": "1.5",
93
+ "aria-hidden": "true"
94
+ }, I = {
95
+ key: 2,
96
+ d: "M20.5 13.5A9 9 0 0 1 10.5 3a9 9 0 1 0 10 10.5Z"
97
+ }, L = /* @__PURE__ */ a({
98
+ __name: "UiSiteHeader",
99
+ props: {
100
+ project: {},
101
+ links: { default: () => [] }
102
+ },
103
+ setup(a) {
104
+ let h = [
105
+ {
106
+ value: "system",
107
+ label: "System"
108
+ },
109
+ {
110
+ value: "light",
111
+ label: "Light"
112
+ },
113
+ {
114
+ value: "dark",
115
+ label: "Dark"
116
+ }
117
+ ], g = t(() => w.find(({ id: e }) => e === a.project)?.label ?? a.project), _ = m().default ? void 0 : C(), v = l(!1), y = l(), b = l(), x = () => {
118
+ y.value && (y.value.open = !1);
119
+ }, S = (e) => {
120
+ e.key !== "Escape" || !y.value?.open || (x(), b.value?.focus());
121
+ }, L = (e) => {
122
+ e.relatedTarget instanceof Node && !y.value?.contains(e.relatedTarget) && x();
123
+ }, R = (e) => {
124
+ e.target instanceof Node && !y.value?.contains(e.target) && x();
125
+ };
126
+ return s(() => {
127
+ v.value = !0, document.addEventListener("pointerdown", R);
128
+ }), o(() => document.removeEventListener("pointerdown", R)), (t, o) => (c(), r("header", T, [i("div", E, [i("details", {
129
+ ref_key: "menu",
130
+ ref: y,
131
+ class: "fg-site-projects",
132
+ onKeydown: S,
133
+ onFocusout: L
134
+ }, [i("summary", {
135
+ ref_key: "trigger",
136
+ ref: b,
137
+ class: "fg-site-projects__trigger",
138
+ "aria-label": `Project: ${g.value}`
139
+ }, [i("span", null, f(g.value), 1), o[0] ||= i("svg", {
140
+ class: "fg-site-projects__chevron",
141
+ viewBox: "0 0 16 16",
142
+ width: "16",
143
+ height: "16",
144
+ fill: "none",
145
+ stroke: "currentColor",
146
+ "stroke-width": "1.5",
147
+ "aria-hidden": "true"
148
+ }, [i("path", { d: "m4 6 4 4 4-4" })], -1)], 8, D), i("nav", O, [(c(!0), r(e, null, u(p(w), (e) => (c(), r("a", {
149
+ key: e.id,
150
+ href: e.href,
151
+ "aria-current": e.id === a.project ? "page" : void 0
152
+ }, f(e.label), 9, k))), 128))])], 544), i("div", A, [a.links.length > 0 ? (c(), r("nav", j, [(c(!0), r(e, null, u(a.links, (e) => (c(), r("a", {
153
+ key: e.href,
154
+ href: e.href
155
+ }, f(e.label), 9, M))), 128))])) : n("", !0), d(t.$slots, "default", {}, () => [v.value && p(_) ? (c(), r("div", N, [(c(), r(e, null, u(h, (t) => i("button", {
156
+ key: t.value,
157
+ type: "button",
158
+ class: "fg-site-appearance__choice",
159
+ "aria-label": `${t.label} theme`,
160
+ title: `${t.label} theme`,
161
+ "aria-pressed": p(_).choice.value === t.value,
162
+ onClick: (e) => p(_).chooseAppearance(t.value)
163
+ }, [(c(), r("svg", F, [t.value === "system" ? (c(), r(e, { key: 0 }, [o[1] ||= i("rect", {
164
+ x: "3",
165
+ y: "4",
166
+ width: "18",
167
+ height: "13",
168
+ rx: "2"
169
+ }, null, -1), o[2] ||= i("path", { d: "M8 21h8m-4-4v4" }, null, -1)], 64)) : t.value === "light" ? (c(), r(e, { key: 1 }, [o[3] ||= i("circle", {
170
+ cx: "12",
171
+ cy: "12",
172
+ r: "4"
173
+ }, null, -1), o[4] ||= i("path", { d: "M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1.5 1.5m11 11L19 19M5 19l1.5-1.5m11-11L19 5" }, null, -1)], 64)) : (c(), r("path", I))]))], 8, P)), 64))])) : n("", !0)])])])]));
174
+ }
175
+ });
176
+ //#endregion
177
+ export { y as SITE_APPEARANCE_KEY, w as SITE_PROJECTS, L as UiSiteHeader, S as applySiteAppearance, x as siteThemeBootScript, C as useSiteAppearance };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@filipgutica/ui",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "A compact Vue design system with Tailwind CSS and VS Code theme support",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -11,6 +11,8 @@
11
11
  "src/theme.css",
12
12
  "src/components.css",
13
13
  "src/components/*.css",
14
+ "src/site.css",
15
+ "src/themes/*.css",
14
16
  "README.md",
15
17
  "LICENSE"
16
18
  ],
@@ -23,12 +25,18 @@
23
25
  "types": "./dist/theme/index.d.ts",
24
26
  "import": "./dist/theme.js"
25
27
  },
28
+ "./site": {
29
+ "types": "./dist/site/index.d.ts",
30
+ "import": "./dist/site.js"
31
+ },
26
32
  "./open-vsx": {
27
33
  "types": "./dist/open-vsx/index.d.ts",
28
34
  "import": "./dist/open-vsx.js"
29
35
  },
30
36
  "./theme.css": "./src/theme.css",
31
- "./components.css": "./src/components.css"
37
+ "./components.css": "./src/components.css",
38
+ "./site.css": "./src/site.css",
39
+ "./themes/graphite-indigo.css": "./src/themes/graphite-indigo.css"
32
40
  },
33
41
  "scripts": {
34
42
  "build": "vite build && vue-tsc -p tsconfig.build.json --emitDeclarationOnly",
@@ -44,8 +52,10 @@
44
52
  "release:dry-run": "semantic-release --dry-run --no-ci",
45
53
  "test": "vitest run",
46
54
  "test:e2e": "playwright test",
55
+ "themes:build": "node scripts/graphite-indigo.mjs",
56
+ "themes:check": "node scripts/graphite-indigo.mjs --check",
47
57
  "typecheck": "vue-tsc -p tsconfig.json --noEmit",
48
- "verify": "pnpm lint && pnpm typecheck && pnpm docs:typecheck && pnpm test && pnpm test:e2e && pnpm build && pnpm docs:build && pnpm pack --dry-run && publint"
58
+ "verify": "pnpm lint && pnpm themes:check && pnpm typecheck && pnpm docs:typecheck && pnpm test && pnpm test:e2e && pnpm build && pnpm docs:build && pnpm pack --dry-run && publint"
49
59
  },
50
60
  "engines": {
51
61
  "node": ">=22.12"
@@ -225,14 +225,14 @@
225
225
  }
226
226
 
227
227
  .fg-button[data-variant="danger"] {
228
- color: var(--color-error);
228
+ color: var(--color-status-text, var(--color-error));
229
229
  background: var(--color-error-surface);
230
230
  box-shadow: none;
231
231
  }
232
232
 
233
233
  .fg-button[data-variant="danger"]:hover:not(:disabled) {
234
234
  border-color: var(--color-error);
235
- color: var(--color-error);
235
+ color: var(--color-status-text, var(--color-error));
236
236
  background: var(--color-error-surface);
237
237
  box-shadow: none;
238
238
  }
@@ -522,11 +522,11 @@
522
522
  line-height: 1.25;
523
523
  }
524
524
 
525
- .fg-badge[data-tone="info"] { color: var(--color-info); background: var(--color-info-surface); }
526
- .fg-badge[data-tone="success"] { color: var(--color-success); background: var(--color-success-surface); }
527
- .fg-badge[data-tone="warning"] { color: var(--color-warning); background: var(--color-warning-surface); }
525
+ .fg-badge[data-tone="info"] { color: var(--color-status-text, var(--color-info)); background: var(--color-info-surface); }
526
+ .fg-badge[data-tone="success"] { color: var(--color-status-text, var(--color-success)); background: var(--color-success-surface); }
527
+ .fg-badge[data-tone="warning"] { color: var(--color-status-text, var(--color-warning)); background: var(--color-warning-surface); }
528
528
  .fg-badge[data-tone="danger"],
529
- .fg-badge[data-tone="error"] { color: var(--color-error); background: var(--color-error-surface); }
529
+ .fg-badge[data-tone="error"] { color: var(--color-status-text, var(--color-error)); background: var(--color-error-surface); }
530
530
 
531
531
  .fg-alert {
532
532
  display: flex;
@@ -545,7 +545,7 @@
545
545
  .fg-alert[data-tone="danger"],
546
546
  .fg-alert[data-tone="error"] {
547
547
  border-color: var(--color-error);
548
- color: var(--color-error);
548
+ color: var(--color-status-text, var(--color-error));
549
549
  background: var(--color-error-surface);
550
550
  }
551
551
 
package/src/site.css ADDED
@@ -0,0 +1,169 @@
1
+ /* Shared chrome and page helpers for the project sites. Use with
2
+ * @filipgutica/ui/themes/graphite-indigo.css, which supplies the type, space, and
3
+ * page-measure roles this file reads. Nothing here targets site-specific markup. */
4
+ @layer components {
5
+ /* A centered frame. The header uses the same edges as the page below it.
6
+ * Set --site-frame on the header or a frame to change its width. */
7
+ .fg-site-frame,
8
+ .fg-site-header__inner {
9
+ width: min(100%, var(--site-frame, var(--page-max)));
10
+ margin-inline: auto;
11
+ padding-inline: var(--site-gutter, var(--space-4));
12
+ }
13
+ .fg-site-frame--wide { --site-frame: var(--page-wide); }
14
+
15
+ .fg-site-header {
16
+ position: relative;
17
+ z-index: 30;
18
+ border-bottom: 1px solid var(--color-border-subtle);
19
+ background: var(--color-bg);
20
+ color: var(--color-text);
21
+ font-family: var(--font-ui);
22
+ font-size: var(--text-md);
23
+ line-height: var(--leading);
24
+ }
25
+ .fg-site-header__inner {
26
+ display: flex;
27
+ min-height: var(--space-16);
28
+ flex-wrap: wrap;
29
+ align-items: center;
30
+ gap: var(--space-2) var(--space-4);
31
+ padding-block: var(--space-2);
32
+ }
33
+ .fg-site-header__end {
34
+ display: flex;
35
+ flex-wrap: wrap;
36
+ align-items: center;
37
+ gap: var(--space-2) var(--space-8);
38
+ margin-inline-start: auto;
39
+ }
40
+ .fg-site-header__links { display: flex; align-items: center; gap: var(--space-6); }
41
+ .fg-site-header__links a {
42
+ display: inline-flex;
43
+ min-height: var(--control-lg);
44
+ align-items: center;
45
+ color: var(--color-muted);
46
+ font-size: var(--text-sm);
47
+ text-decoration: none;
48
+ }
49
+ .fg-site-header__links a:hover { color: var(--color-text); }
50
+
51
+ .fg-site-projects { position: relative; }
52
+ .fg-site-projects__trigger {
53
+ display: inline-flex;
54
+ min-height: var(--control-lg);
55
+ align-items: center;
56
+ gap: var(--space-1);
57
+ padding-inline: var(--space-3);
58
+ border-radius: var(--radius-control);
59
+ color: var(--color-text);
60
+ font-weight: var(--weight-semibold);
61
+ list-style: none;
62
+ cursor: pointer;
63
+ user-select: none;
64
+ }
65
+ .fg-site-projects__trigger::-webkit-details-marker { display: none; }
66
+ .fg-site-projects__trigger:hover,
67
+ .fg-site-projects[open] > .fg-site-projects__trigger { background: var(--color-row-hover); }
68
+ .fg-site-projects__chevron { transition: transform var(--duration-fast) ease; }
69
+ .fg-site-projects[open] .fg-site-projects__chevron { transform: rotate(180deg); }
70
+ .fg-site-projects__menu {
71
+ position: absolute;
72
+ z-index: 1;
73
+ top: 100%;
74
+ left: 0;
75
+ min-width: 14rem;
76
+ margin-top: var(--space-1);
77
+ padding: var(--space-1);
78
+ border-radius: var(--radius-panel);
79
+ background: var(--color-surface);
80
+ box-shadow: var(--shadow-dialog);
81
+ }
82
+
83
+ /* Route rows. Hover is neutral, and the current page keeps its selected fill on
84
+ * hover. Only aria-current="page" is styled: an in-page section marker
85
+ * (aria-current="location") must look different from the current page. */
86
+ .fg-site-nav a {
87
+ display: flex;
88
+ min-height: var(--row);
89
+ align-items: center;
90
+ padding-inline: var(--space-3);
91
+ border-radius: var(--radius-control);
92
+ color: var(--color-muted);
93
+ font-size: var(--text-md);
94
+ text-decoration: none;
95
+ transition: background-color var(--duration-fast) ease, color var(--duration-fast) ease;
96
+ }
97
+ .fg-site-nav a:hover { background: var(--color-row-hover); color: var(--color-text); }
98
+ .fg-site-nav a[aria-current="page"] {
99
+ background: var(--color-row-selected);
100
+ box-shadow: inset 2px 0 0 var(--color-accent);
101
+ color: var(--color-text);
102
+ font-weight: var(--weight-medium);
103
+ }
104
+
105
+ .fg-site-appearance {
106
+ display: inline-flex;
107
+ flex: none;
108
+ border: 1px solid var(--color-border-subtle);
109
+ border-radius: var(--radius-control);
110
+ }
111
+ .fg-site-appearance__choice {
112
+ display: inline-flex;
113
+ width: var(--control-lg);
114
+ min-height: var(--control-lg);
115
+ align-items: center;
116
+ justify-content: center;
117
+ padding: 0;
118
+ border: 0;
119
+ color: var(--color-muted);
120
+ background: transparent;
121
+ font: inherit;
122
+ cursor: pointer;
123
+ }
124
+ .fg-site-appearance__choice + .fg-site-appearance__choice { border-left: 1px solid var(--color-border-subtle); }
125
+ .fg-site-appearance__choice:first-child { border-radius: var(--radius-control) 0 0 var(--radius-control); }
126
+ .fg-site-appearance__choice:last-child { border-radius: 0 var(--radius-control) var(--radius-control) 0; }
127
+ .fg-site-appearance__choice:hover { color: var(--color-text); background: var(--color-row-hover); }
128
+ .fg-site-appearance__choice[aria-pressed="true"] { color: var(--color-text); background: var(--color-row-selected); }
129
+
130
+ .fg-site-header a:focus-visible,
131
+ .fg-site-projects__trigger:focus-visible,
132
+ .fg-site-appearance__choice:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
133
+ .fg-site-appearance__choice:focus-visible { outline-offset: -2px; }
134
+ .fg-site-nav a:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -2px; }
135
+
136
+ /* Type roles for article content. Prose reads at --text-reading. Components set
137
+ * their own size, and controls and dense content such as tables belong at --text-md.
138
+ * Zero-specificity heading selectors keep component and page styles in charge. */
139
+ .fg-site-prose { font-size: var(--text-reading); line-height: var(--leading); }
140
+ .fg-site-prose :where(h1, h2, h3, h4) {
141
+ margin: 0;
142
+ font-weight: var(--weight-semibold);
143
+ line-height: var(--leading-tight);
144
+ letter-spacing: var(--tracking-tight);
145
+ text-wrap: balance;
146
+ }
147
+ .fg-site-prose :where(h1) { font-size: var(--text-3xl); }
148
+ .fg-site-prose :where(h2) { font-size: var(--text-xl); }
149
+ .fg-site-prose :where(h3, h4) { font-size: var(--text-lg); }
150
+ .fg-site-prose :where(p, ul, ol) { max-width: var(--measure); margin: 0; }
151
+ .fg-site-prose :where(p, ul, ol) + :where(p, ul, ol) { margin-top: var(--space-4); }
152
+ .fg-site-prose :where(p a, li a, dd a) { text-decoration: underline; text-underline-offset: 0.18em; }
153
+ .fg-site-meta { color: var(--color-muted); font-size: var(--text-sm); }
154
+
155
+ @media (min-width: 681px) {
156
+ .fg-site-frame,
157
+ .fg-site-header__inner { padding-inline: var(--site-gutter, var(--space-6)); }
158
+ }
159
+ @media (max-width: 680px) {
160
+ .fg-site-prose :where(h1) { font-size: var(--text-2xl); }
161
+ .fg-site-header__end { gap: var(--space-2) var(--space-4); }
162
+ .fg-site-header__links { gap: var(--space-4); }
163
+ .fg-site-projects__menu a { min-height: var(--control-lg); }
164
+ }
165
+ @media (prefers-reduced-motion: reduce) {
166
+ .fg-site-projects__chevron,
167
+ .fg-site-nav a { transition: none; }
168
+ }
169
+ }
@@ -0,0 +1,111 @@
1
+ /* Generated from themes/graphite-indigo.json by scripts/graphite-indigo.mjs. Do not edit.
2
+ * Opt-in Graphite Indigo roles for @filipgutica/ui. Import after theme.css.
3
+ * Dark mode uses the same .dark class as theme.css. Keep exactly one :root.dark block:
4
+ * sites copy it into a <noscript> prefers-color-scheme rule. */
5
+
6
+ :root {
7
+ color-scheme: light;
8
+ --color-bg: #f4f5f8;
9
+ --color-surface: #e7e8ec;
10
+ --color-surface-raised: #ffffff;
11
+ --color-surface-muted: #e4e6eb;
12
+ --color-text: #222326;
13
+ --color-muted: #5d626c;
14
+ --color-link: #075fc7;
15
+ --color-accent: #6359a7;
16
+ --color-focus: #075fc7;
17
+ --color-border: #777d88;
18
+ --color-border-subtle: #d8dbe2;
19
+ --color-button-bg: var(--color-accent);
20
+ --color-button-text: #ffffff;
21
+ --color-button-hover: #554b94;
22
+ --color-input-bg: var(--color-surface-raised);
23
+ --color-input-text: var(--color-text);
24
+ --color-input-border: var(--color-border);
25
+ --color-input-placeholder: var(--color-muted);
26
+ --color-row-hover: #dfe1e7;
27
+ --color-row-selected: #ffffff;
28
+ --color-row-selected-text: var(--color-text);
29
+ --color-code: var(--color-surface);
30
+ --color-code-text: var(--color-text);
31
+ --color-syntax-comment: #626772;
32
+ --color-syntax-keyword: #715b42;
33
+ --color-syntax-string: #346347;
34
+ --color-syntax-number: #86532e;
35
+ --color-syntax-function: var(--color-syntax-keyword);
36
+ --color-syntax-type: var(--color-syntax-number);
37
+ --color-syntax-variable: var(--color-code-text);
38
+ --color-syntax-operator: var(--color-muted);
39
+ --color-syntax-punctuation: var(--color-muted);
40
+ --color-syntax-tag: var(--color-syntax-keyword);
41
+ --color-syntax-attribute: var(--color-syntax-number);
42
+ --color-error: #c7372d;
43
+ --color-warning: #86532e;
44
+ --color-success: #1f8a5b;
45
+ --color-info: #175cd3;
46
+ --color-progress: var(--color-accent);
47
+ --color-error-surface: color-mix(in srgb, var(--color-error) 10%, var(--color-bg));
48
+ --color-warning-surface: color-mix(in srgb, var(--color-warning) 12%, var(--color-bg));
49
+ --color-success-surface: color-mix(in srgb, var(--color-success) 11%, var(--color-bg));
50
+ --color-info-surface: color-mix(in srgb, var(--color-info) 10%, var(--color-bg));
51
+ --shadow-dialog: 0 8px 24px rgb(16 16 24 / 12%), 0 0 0 1px var(--color-border-subtle);
52
+ --color-status-text: var(--color-text);
53
+ --text-3xl: 32px;
54
+ --text-reading: 16px;
55
+ --text-display: 56px;
56
+ --text-display-sm: 38px;
57
+ --tracking-tight: -0.01em;
58
+ --space-20: 80px;
59
+ --space-24: 96px;
60
+ --row: 36px;
61
+ --page-wide: 1120px;
62
+ }
63
+
64
+ :root.dark {
65
+ color-scheme: dark;
66
+ --color-bg: #151619;
67
+ --color-surface: #191a1d;
68
+ --color-surface-raised: #212226;
69
+ --color-surface-muted: #303238;
70
+ --color-text: #f4f5f8;
71
+ --color-muted: #b0b5bf;
72
+ --color-link: #58a6ff;
73
+ --color-accent: #a99ce2;
74
+ --color-focus: #58a6ff;
75
+ --color-border: #818894;
76
+ --color-border-subtle: #3b3e45;
77
+ --color-button-bg: var(--color-accent);
78
+ --color-button-text: #151619;
79
+ --color-button-hover: #b7ace9;
80
+ --color-input-bg: var(--color-surface-raised);
81
+ --color-input-text: var(--color-text);
82
+ --color-input-border: var(--color-border);
83
+ --color-input-placeholder: var(--color-muted);
84
+ --color-row-hover: #25272d;
85
+ --color-row-selected: #38334f;
86
+ --color-row-selected-text: var(--color-text);
87
+ --color-code: var(--color-surface);
88
+ --color-code-text: var(--color-text);
89
+ --color-syntax-comment: #a0a6b1;
90
+ --color-syntax-keyword: #d8c2a5;
91
+ --color-syntax-string: #a8bd9c;
92
+ --color-syntax-number: #d5b98c;
93
+ --color-syntax-function: var(--color-syntax-keyword);
94
+ --color-syntax-type: var(--color-syntax-number);
95
+ --color-syntax-variable: var(--color-code-text);
96
+ --color-syntax-operator: var(--color-muted);
97
+ --color-syntax-punctuation: var(--color-muted);
98
+ --color-syntax-tag: var(--color-syntax-keyword);
99
+ --color-syntax-attribute: var(--color-syntax-number);
100
+ --color-error: #f0776c;
101
+ --color-warning: #e2a54b;
102
+ --color-success: #4cc38a;
103
+ --color-info: #84adff;
104
+ --color-progress: var(--color-accent);
105
+ --color-error-surface: color-mix(in srgb, var(--color-error) 10%, var(--color-bg));
106
+ --color-warning-surface: color-mix(in srgb, var(--color-warning) 12%, var(--color-bg));
107
+ --color-success-surface: color-mix(in srgb, var(--color-success) 11%, var(--color-bg));
108
+ --color-info-surface: color-mix(in srgb, var(--color-info) 10%, var(--color-bg));
109
+ --shadow-dialog: 0 8px 24px rgb(0 0 0 / 45%), 0 0 0 1px var(--color-border);
110
+ --color-status-text: var(--color-text);
111
+ }