Skip to content

Commit 5fa620e

Browse files
docs(editor): Refresh colour, shadow, and type style guide pages (no-changelog) (#37504)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6586dbe commit 5fa620e

9 files changed

Lines changed: 829 additions & 267 deletions

File tree

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Meta } from '@storybook/addon-docs/blocks';
22

3+
import NamedTokensColorTable from './components/NamedTokensColorTable.vue';
34
import PrimitivesColorTable from './components/PrimitivesColorTable.vue';
45

5-
<Meta title="Style guide/Colour" />
6+
<Meta title="Style Guide/Colour" />
67

78
# Colour
89

@@ -18,35 +19,6 @@ Each colour has 13 steps ranging from lightest (50) to darkest (950).
1819

1920
Instead of using the raw primitive colours, it's often better to use a token. These are aliases for common use cases (e.g text color) to make it easier to keep UI consistent and flexible.
2021

21-
### Text
22-
23-
| Token | Description |
24-
| ----------------------- | ----------------------------------------------- |
25-
| `--text-color` | Default body text and primary labels. |
26-
| `--text-color--subtle` | Secondary text such as helper copy or metadata. |
27-
| `--text-color--subtler` | Tertiary or low-emphasis text. |
28-
| `--text-color--inverse` | Text shown on dark or inverse surfaces. |
29-
30-
### Background
31-
32-
| Token | Description |
33-
| ----------------------- | ------------------------------------------------------ |
34-
| `--background--surface` | Base surface for cards, panels, and page sections. |
35-
| `--background--hover` | Hover state for neutral interactive surfaces. |
36-
| `--background--active` | Pressed/active state for neutral interactive surfaces. |
37-
| `--background--success` | Success banners, badges, or confirmation surfaces. |
38-
| `--background--warning` | Warning callouts and cautionary surfaces. |
39-
| `--background--danger` | Danger/error surfaces behind destructive messaging. |
40-
| `--background--info` | Informational surfaces and neutral notices. |
41-
42-
### Border
43-
44-
| Token | Description |
45-
| ------------------------- | ------------------------------------------------- |
46-
| `--border-color` | Default border for inputs, cards, and separators. |
47-
| `--border-color--subtle` | Low-contrast dividers and lightweight outlines. |
48-
| `--border-color--strong` | More prominent outlines for elevated contrast. |
49-
| `--border-color--success` | Borders for success states and confirmations. |
50-
| `--border-color--warning` | Borders for warning states and cautions. |
51-
| `--border-color--danger` | Borders for error states and destructive actions. |
52-
| `--border-color--info` | Borders for informational states. |
22+
<div class="sb-unstyled">
23+
<NamedTokensColorTable />
24+
</div>
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
<script setup lang="ts">
2+
import { computed, onMounted, onUnmounted, ref } from 'vue';
3+
4+
import N8nIcon from '../../components/N8nIcon/Icon.vue';
5+
import N8nInput from '../../components/N8nInput/Input.vue';
6+
import tokensSource from '../../css/_tokens.scss?raw';
7+
import { getColorTokenNames } from '../utils/cssTokenSource';
8+
9+
const SEMANTIC_COLOR_TOKENS = getColorTokenNames(tokensSource);
10+
11+
type TokenGroup = {
12+
label: string;
13+
tokens: string[];
14+
};
15+
16+
const GROUP_ORDER = ['Text Color', 'Background', 'Border Color', 'Icon Color', 'Color', 'Focus'];
17+
18+
const query = ref('');
19+
const tokenValues = ref<Record<string, string>>({});
20+
21+
let observer: MutationObserver | null = null;
22+
let colorSchemeQuery: MediaQueryList | null = null;
23+
24+
const groupLabelFor = (token: string) => {
25+
const firstGroup = token.slice(2).split('--')[0] ?? token;
26+
return firstGroup
27+
.split('-')
28+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
29+
.join(' ');
30+
};
31+
32+
const allNamedTokens = computed(() =>
33+
[...SEMANTIC_COLOR_TOKENS].sort((a, b) => a.localeCompare(b)),
34+
);
35+
36+
const filteredTokens = computed(() => {
37+
const needle = query.value.trim().toLowerCase();
38+
39+
if (!needle) {
40+
return allNamedTokens.value;
41+
}
42+
43+
return allNamedTokens.value.filter((token) => {
44+
const value = tokenValues.value[token] ?? '';
45+
return token.toLowerCase().includes(needle) || value.toLowerCase().includes(needle);
46+
});
47+
});
48+
49+
const groupedTokens = computed((): TokenGroup[] => {
50+
const groups = new Map<string, string[]>();
51+
52+
for (const token of filteredTokens.value) {
53+
const label = groupLabelFor(token);
54+
const tokens = groups.get(label) ?? [];
55+
tokens.push(token);
56+
groups.set(label, tokens);
57+
}
58+
59+
return [...groups.entries()]
60+
.map(([label, tokens]) => ({ label, tokens }))
61+
.sort((a, b) => {
62+
const aOrder = GROUP_ORDER.indexOf(a.label);
63+
const bOrder = GROUP_ORDER.indexOf(b.label);
64+
65+
if (aOrder !== bOrder) {
66+
if (aOrder === -1) {
67+
return 1;
68+
}
69+
if (bOrder === -1) {
70+
return -1;
71+
}
72+
return aOrder - bOrder;
73+
}
74+
75+
return a.label.localeCompare(b.label);
76+
});
77+
});
78+
79+
const updateValues = () => {
80+
const style = getComputedStyle(document.body);
81+
const nextValues: Record<string, string> = {};
82+
83+
for (const token of SEMANTIC_COLOR_TOKENS) {
84+
nextValues[token] = style.getPropertyValue(token).trim();
85+
}
86+
87+
tokenValues.value = nextValues;
88+
};
89+
90+
onMounted(() => {
91+
updateValues();
92+
93+
observer = new MutationObserver((mutationsList) => {
94+
for (const mutation of mutationsList) {
95+
if (mutation.type === 'attributes') {
96+
updateValues();
97+
}
98+
}
99+
});
100+
101+
observer.observe(document.body, { attributes: true });
102+
103+
if (typeof window.matchMedia === 'function') {
104+
colorSchemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
105+
colorSchemeQuery.addEventListener('change', updateValues);
106+
}
107+
});
108+
109+
onUnmounted(() => {
110+
observer?.disconnect();
111+
colorSchemeQuery?.removeEventListener('change', updateValues);
112+
});
113+
</script>
114+
115+
<template>
116+
<div :class="$style.container">
117+
<div :class="$style.search">
118+
<N8nInput v-model="query" size="small" placeholder="Search" clearable>
119+
<template #prefix>
120+
<N8nIcon icon="search" :size="14" />
121+
</template>
122+
</N8nInput>
123+
</div>
124+
125+
<div v-if="filteredTokens.length === 0" :class="$style.empty">
126+
No named tokens match that search.
127+
</div>
128+
129+
<section v-for="group in groupedTokens" :key="group.label" :class="$style.group">
130+
<div :class="$style.groupLabel">{{ group.label }}</div>
131+
<ul :class="$style.list">
132+
<li v-for="token in group.tokens" :key="token" :class="$style.item">
133+
<div :class="$style.row">
134+
<span :class="$style.swatch" aria-hidden="true">
135+
<span :class="$style.swatchFill" :style="{ background: `var(${token})` }" />
136+
</span>
137+
<span :class="$style.name">{{ token }}</span>
138+
<span :class="$style.value">{{ tokenValues[token] }}</span>
139+
</div>
140+
</li>
141+
</ul>
142+
</section>
143+
</div>
144+
</template>
145+
146+
<style lang="scss" module>
147+
.container {
148+
display: flex;
149+
flex-direction: column;
150+
gap: var(--spacing--lg);
151+
margin: var(--spacing--xl) 0;
152+
}
153+
154+
.search {
155+
width: fit-content;
156+
max-width: 100%;
157+
158+
> :global(*) {
159+
width: auto;
160+
}
161+
162+
:global(input) {
163+
flex: none;
164+
width: 20ch;
165+
}
166+
}
167+
168+
.empty,
169+
.groupLabel,
170+
.name,
171+
.value {
172+
margin: 0;
173+
color: var(--text-color--subtle);
174+
}
175+
176+
.empty {
177+
font-size: var(--font-size--sm);
178+
}
179+
180+
.group {
181+
display: flex;
182+
flex-direction: column;
183+
gap: var(--spacing--xs);
184+
}
185+
186+
.groupLabel {
187+
font-size: var(--font-size--sm);
188+
font-weight: var(--font-weight--medium);
189+
color: var(--text-color);
190+
}
191+
192+
.list {
193+
display: flex;
194+
flex-direction: column;
195+
gap: var(--spacing--4xs);
196+
margin: 0;
197+
padding: 0;
198+
list-style: none;
199+
}
200+
201+
.item {
202+
display: block;
203+
}
204+
205+
.row {
206+
display: grid;
207+
grid-template-columns: var(--spacing--xl) minmax(0, 1.2fr) minmax(0, 1fr);
208+
align-items: center;
209+
gap: var(--spacing--sm);
210+
width: 100%;
211+
}
212+
213+
.swatch {
214+
display: block;
215+
width: var(--spacing--xl);
216+
height: var(--spacing--xl);
217+
border-radius: var(--radius);
218+
box-shadow: var(--shadow--outline);
219+
background-color: var(--background--surface);
220+
overflow: hidden;
221+
}
222+
223+
.swatchFill {
224+
display: inline-flex;
225+
align-items: center;
226+
justify-content: center;
227+
width: 100%;
228+
height: 100%;
229+
}
230+
231+
.name,
232+
.value {
233+
font-family: var(--font-family--monospace);
234+
font-size: var(--font-size--2xs);
235+
line-height: var(--line-height--sm);
236+
overflow: hidden;
237+
text-overflow: ellipsis;
238+
white-space: nowrap;
239+
}
240+
241+
.name {
242+
color: var(--text-color);
243+
}
244+
245+
.value {
246+
color: var(--text-color--subtle);
247+
}
248+
</style>

0 commit comments

Comments
 (0)