@holistics/anfra-design 0.16.0 → 0.17.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/DESIGN.md CHANGED
@@ -194,7 +194,7 @@ Colors are in `tokens.css`, in two tiers, after the Holistics Design System. A h
194
194
  | --- | --- | --- |
195
195
  | 1 Primitives | `--anfra-{ink,paper,violet,red,amber,green,white,black}-{step}`, OKLCH | Tier 2 only |
196
196
  | 2 Semantic | `--anfra-color-{scope}-{semantic}-{state}`, scope = `background`, `text`, `icon`, `border`, `focus-ring` | Every component, in every host |
197
- | 3 Host aliases | Cloud: Tailwind's `text-weak`, `bg-hover`, shadcn's `--primary`. appserve: its stylesheet | That host's components |
197
+ | 3 Host aliases | Tailwind's `text-weak`, `bg-hover`, shadcn's `--primary`, in Cloud and appserve alike | That host's components |
198
198
 
199
199
  The same word is a different color for each scope: `--anfra-color-text-weak`, `--anfra-color-background-hover`, `--anfra-color-border-strong`, `--anfra-color-icon-weak`. Below, a token is named without its `--anfra-color-` prefix.
200
200
 
@@ -204,8 +204,9 @@ The same word is a different color for each scope: `--anfra-color-text-weak`, `-
204
204
  - **Status** is restrained red, amber and green; info is neutral ink, never blue or violet. A status always has a label or an icon, and never fills a large surface: `background-{danger,warning,success,info}-secondary-default` behind `text-{s}-default`.
205
205
  - **Disabled** is `background-disabled` and `text-disabled`, plus `border-disabled` on a control with a border. Never lower the opacity instead.
206
206
  - **Neutral states** are `background-hover` and `background-active`; they work on paper and on white.
207
+ - **Fills**: a filled control (the secondary button, a tab list's track) rests on `background-fill-default` and hovers on `background-fill-hover`; the open tab is `background-fill-selected`. A fill is a tint of whatever it sits on, so the same control reads on the panel, a card or a well.
207
208
  - **Errors**: `text-danger-default` {colors.error-text} for error text; `background-danger-default` {colors.error} is the solid fill, never text.
208
- - **Dark** redefines Tier 2 only, under `.dark` or `[data-theme="dark"]` on `<html>`. A host follows the system by default; light and dark are choices, stored per device.
209
+ - **Dark** redefines Tier 2 only, under `.dark` or `[data-theme="dark"]` on `<html>`. A host follows the system by default; light and dark are choices, stored per device. Dark is its own set of surfaces, not the light values swapped: see Elevation.
209
210
 
210
211
  ## Typography
211
212
 
@@ -237,20 +238,53 @@ One density for every screen, on a 4px unit {spacing.unit}.
237
238
 
238
239
  ## Elevation & Depth
239
240
 
240
- Depth comes from tone and hairlines: paper behind, white in front, 1px rules between. Shadows are neutral ink, never colored, and only two exist: `--anfra-e1` for a resting card, `--anfra-e2` for what floats (menus, popovers, dialogs). On dark, both become a border.
241
+ Depth comes from tone and hairlines: paper behind, white in front, 1px rules between. Shadows are neutral ink, never colored, and only two exist: `--anfra-e1` for a resting card, `--anfra-e2` for what floats (menus, popovers, dialogs). `--anfra-e2` carries its own edge, a translucent 1px ring, so a menu needs no border and reads on any ground. On dark, a shadow doesn't show: e1 becomes a border and e2 a translucent white ring.
242
+
243
+ **On dark, lighter is closer.** Each layer up is a step lighter: the chrome (`ink-1000`), the panel (`ink-925`), a card (`ink-875`), what floats (`ink-825`). A well is recessed, so it is darker than the panel it sits in. A control is not: its fill is a translucent white over its ground, so a secondary button or a tab list's track is lighter than the ground under it, and the open tab lighter again. In light the same fills are a translucent ink, a tint just darker than the ground, since nothing is lighter than white; there the open tab is white, raised by e1.
244
+
245
+ | Role | Light | Dark |
246
+ | --- | --- | --- |
247
+ | A control's fill (`background-fill-default`) | ink 4% over the ground | white 4% over the ground |
248
+ | Hover on it (`background-fill-hover`) | ink 8% | white 8% |
249
+ | The open tab (`background-fill-selected`) | white, e1 | white 8% over the track, e1 |
241
250
 
242
251
  ## Shapes
243
252
 
244
253
  Soft, not round. Controls and surfaces nest by radius: the smaller the thing, the smaller the corner.
245
254
 
246
- - `--anfra-radius-md` {rounded.md}: menu items, nav items, chips, tooltips.
247
- - `--anfra-radius-lg` {rounded.lg}: buttons, fields, alerts.
255
+ - `--anfra-radius-md` {rounded.md}: nav items, chips, tooltips, and controls under 32px (xs and sm buttons, the small tab list).
256
+ - `--anfra-radius-lg` {rounded.lg}: buttons, fields, alerts, at 32px and up.
248
257
  - `--anfra-radius-xl` {rounded.xl}: cards, popovers, dialogs, the panel.
249
258
  - `--anfra-radius-pill` {rounded.pill}: status pills and counters only, never buttons.
250
259
 
260
+ **Nested corners are concentric.** A part that sits in a container's corner, closer to its edge than the container's radius, takes the container's radius less the gap between them, written as that `calc()` so it follows the tokens. A part flush with the edge takes the container's radius; a part further in than the radius takes its own from the scale. The gap is even on the sides the part is near, so the track around it is too.
261
+
262
+ | Part, in its container | Gap | Radius |
263
+ | --- | --- | --- |
264
+ | A tab in its list | 3px | `lg` − 3 = 7px |
265
+ | A tab in a small list (`data-size="sm"`) | 2px | `md` − 2 = 4px |
266
+ | A menu item in its menu, a palette entry in the palette | 4px | `xl` − 4 = 10px |
267
+ | A key hint in a field | 6px | `lg` − 6 = 4px |
268
+
269
+ `components.test.mjs` holds the class layer's pairs to this.
270
+
251
271
  ## Components
252
272
 
253
- Each host builds these from its own toolkit (Cloud: shadcn-vue; appserve and the SDK: plain HTML and CSS), and each looks the same.
273
+ Cloud and appserve build these with shadcn-vue (appserve's are in `web/appserve/src/components/ui`). The SDK's plugin panel is framework-free DOM in shadow roots, with no component library, so it uses the class layer, `components.css`: each class draws what the shadcn component of its name draws in Cloud, from the same Tier 2 tokens. Variants and sizes are data attributes, as in shadcn.
274
+
275
+ | Class | Cloud's component | Variants |
276
+ | --- | --- | --- |
277
+ | `.a-button` | Button | `data-variant` default, outline, secondary, ghost, destructive, link; `data-size` xs, sm, default, lg, icon, icon-xs, icon-sm, icon-lg |
278
+ | `.a-input` | Input | `aria-invalid="true"` for an error; a field with an icon is a `label.a-input` around the icon and a bare `input` |
279
+ | `.a-badge` | Badge | `data-variant` default, secondary, outline, destructive, warning, success; `data-size="icon"` for a badge holding one icon or glyph, square |
280
+ | `.a-pill` | StatusPill | `data-tone` neutral, highlight, success, warning, danger |
281
+ | `.a-tabs`, `.a-tab` | TabsList, TabsTrigger | `data-variant="line"` on the list; `data-size="sm"` for a 24px list with a 2px track; the open tab is `aria-selected="true"`, or `aria-pressed="true"` in a group of toggles |
282
+ | `.a-menu`, `.a-menu-item`, `.a-menu-label`, `.a-menu-separator`, `.a-menu-shortcut` | DropdownMenu | `data-variant="destructive"` on an item; `data-highlighted` for a keyboard highlight |
283
+ | `.a-tooltip` | TooltipContent | |
284
+ | `.a-kbd`, `.a-kbd-group` | Kbd, KbdGroup | a combination is a group, one Kbd per key |
285
+ | `.a-nav-label`, `.a-nav-item`, `.a-tree`, `.a-tree-row` | SidebarGroupLabel, SidebarMenuButton, SidebarMenuSub, SidebarMenuSubButton | `data-size="sm"` on a nav item; the current page is `aria-current="page"` |
286
+
287
+ A change to a Cloud component's look is a change to its class here, in the same release.
254
288
 
255
289
  ### Buttons
256
290
 
@@ -268,7 +302,7 @@ Each host builds these from its own toolkit (Cloud: shadcn-vue; appserve and the
268
302
 
269
303
  ### Fields
270
304
 
271
- - **Rest**: white, `border-field`, the body role, `text-placeholder` for the placeholder.
305
+ - **Rest**: white, `border-strong` (the outline button's edge), the body role, `text-placeholder` for the placeholder. **Hover**: `border-hover`. A checkbox or a switch track keeps `border-field`, its 3:1 boundary.
272
306
  - **Focus**: no outline; the border turns violet (`border-highlight-strong`) and doubles to 2px.
273
307
  - **Error**: the border is red (`border-danger-strong`) at rest and on focus, never violet. The message is `text-danger-default` under the field and clears as soon as the user types again.
274
308
 
@@ -276,16 +310,18 @@ Each host builds these from its own toolkit (Cloud: shadcn-vue; appserve and the
276
310
 
277
311
  - **Current nav item**: on the paper, a white chip with a violet icon; in a nav inside the panel (tabs, settings), `background-active` and weight 500. Both carry `aria-current="page"` or `aria-selected`.
278
312
  - **Alerts**: a tinted container with an icon and the body role.
313
+ - **A control keeps its edge on any ground**: a control's fill is translucent (`background-fill-*`), never a surface color, so a tab list or a secondary button in a well (`background-secondary-default`) still stands off it.
279
314
  - **Badges and status pills**: caption at 500, a label or an icon always beside the color.
280
315
  - **Tooltips**: inverse ink (`background-inverted-default`, `text-inverted-default`), the caption role, `--anfra-radius-md`.
316
+ - **Shortcuts**: a combination is a KbdGroup, one Kbd per key, beside the command it runs (a field's hint, a palette entry). On a Mac the modifiers are its symbols (⌘ ⌥ ⇧ ⌃), elsewhere their names (Ctrl, Alt, Shift).
281
317
 
282
318
  ### Icons
283
319
 
284
- Lucide, at 16px in controls and 14px in dense rows, `currentColor`, never filled. Core draws the same icons from their path data, so it carries no icon library.
320
+ Lucide, at 16px in controls and 14px in dense rows, `currentColor`, a 2px round stroke, never filled. Cloud and appserve import them from `@lucide/vue`. The SDK's plugin panel, framework-free, draws the same icons from their path data, so it carries no icon library: `@holistics/anfra-design/icons` exports `ICONS`, each icon's path data on a 24px grid, by the name it has in anfra (`sidebar`, `lineage`, `type-date`), generated from lucide by `scripts/icons.mjs`. To add one, add its name and lucide's to that script and run `pnpm --filter @holistics/anfra-design icons`.
285
321
 
286
322
  ### Logo
287
323
 
288
- The product shows the logomark only: violet on light, the inverse on dark, at the top of the sidebar. Never set the mark beside typed "anfra".
324
+ The product shows the logomark only: violet on light, the inverse on dark (`--anfra-color-icon-logo`), at the top of the sidebar. Never set the mark beside typed "anfra".
289
325
 
290
326
  ## Do's and Don'ts
291
327
 
@@ -307,6 +343,7 @@ The product shows the logomark only: violet on light, the inverse on dark, at th
307
343
 
308
344
  - "anfra" is lowercase, even first in a sentence.
309
345
  - Text of one sentence has no end period; two or more sentences keep all of theirs.
346
+ - Labels, headings and group labels are sentence case. Never uppercase text with CSS; only the mono-label role (kinds, revisions) is uppercase.
310
347
  - Users read "organization", never "org", "tenant" or "workspace". Code and URLs keep `org`.
311
348
 
312
349
  ## Accessibility
@@ -317,9 +354,9 @@ The product shows the logomark only: violet on light, the inverse on dark, at th
317
354
 
318
355
  ## Implementation
319
356
 
320
- - **Package**: `@holistics/anfra-design`, released with anfra at its version. `index.css` imports `fonts.css`, `tokens.css` and `type.css`.
357
+ - **Package**: `@holistics/anfra-design`, released with anfra at its version. `index.css` imports `fonts.css`, `tokens.css`, `type.css` and `components.css`; `icons` is a module.
321
358
  - **anfra Cloud** imports it in `web/src/main.css` and maps Tier 2 onto Tailwind and shadcn's aliases there.
322
- - **appserve** imports it in `web/appserve/src/style.css`; its stylesheet uses Tier 2 tokens only.
359
+ - **appserve** imports `fonts.css`, `tokens.css` and `type.css` in `web/appserve/src/main.ts`, and maps Tier 2 onto Tailwind and shadcn's aliases in `web/appserve/src/theme.css`, as Cloud does; it doesn't import the class layer.
323
360
  - **The SDK's plugin UI** reads the theme a host passes (`--anfra-color-bg` and the rest, `web/sdk/src/common/theme.ts`); a host takes those from Tier 2.
324
361
  - **To change a rule or a token**: a PR here, with `tokens.css` and this file in step; then each host takes the release. A rule for one page goes in that page's spec, not here.
325
- - **Checks**: `pnpm --filter @holistics/anfra-design test` (every reference resolves, dark redefines only Tier 2, only Tier 1 holds raw colors); `npx @google/design.md lint web/design/DESIGN.md`.
362
+ - **Checks**: `pnpm --filter @holistics/anfra-design test` (every reference resolves, dark redefines only Tier 2, only Tier 1 holds raw colors, the class layer reads only Tier 2, `icons.js` is what the script writes); `npx @google/design.md lint web/design/DESIGN.md`.
package/components.css ADDED
@@ -0,0 +1,440 @@
1
+ /*
2
+ * The class layer: anfra's components as plain CSS, for framework-free UI that has no Vue or React
3
+ * to draw shadcn with: the SDK's plugin panel, DOM in shadow roots. A Vue app (appserve, anfra
4
+ * Cloud) uses the shadcn-vue components instead. Each class draws what anfra Cloud's shadcn
5
+ * component of the same name draws, variant for variant, on the Tier 2 tokens alone, so a token
6
+ * change reaches both. Variants and sizes are data attributes:
7
+ *
8
+ * <button class="a-button" data-variant="outline" data-size="sm">Save</button>
9
+ *
10
+ * A page that uses it wears tokens.css and type.css too (index.css has all three). A shadow root
11
+ * gets the tokens from its page, or from a host that sets them on it (the SDK does, from its theme).
12
+ * Hover applies only where there is one, as Tailwind's hover: does.
13
+ */
14
+
15
+ /* Button: shadcn's Button. Variants default (filled), outline, secondary, ghost, destructive, link;
16
+ sizes xs, sm, default, lg, and icon-xs, icon-sm, icon, icon-lg (square). */
17
+ .a-button {
18
+ box-sizing: border-box;
19
+ display: inline-flex;
20
+ flex-shrink: 0;
21
+ align-items: center;
22
+ justify-content: center;
23
+ gap: 6px;
24
+ height: 32px;
25
+ padding: 0 10px;
26
+ border: 1px solid transparent;
27
+ border-radius: var(--anfra-radius-lg);
28
+ background-clip: padding-box;
29
+ background-color: var(--anfra-color-background-highlight-default);
30
+ color: var(--anfra-color-text-on-fill);
31
+ font: var(--anfra-type-label);
32
+ white-space: nowrap;
33
+ text-decoration: none;
34
+ cursor: pointer;
35
+ user-select: none;
36
+ outline: none;
37
+ transition: background-color 160ms var(--anfra-ease-out), border-color 160ms var(--anfra-ease-out),
38
+ color 160ms var(--anfra-ease-out), scale 160ms var(--anfra-ease-out);
39
+ }
40
+ .a-button svg { flex-shrink: 0; width: 16px; height: 16px; pointer-events: none; }
41
+ .a-button:active:not([aria-haspopup]) { scale: 0.97; transition-duration: 90ms; }
42
+ .a-button:is(:disabled, [aria-disabled="true"]) {
43
+ pointer-events: none;
44
+ border-color: var(--anfra-color-border-disabled);
45
+ background-color: var(--anfra-color-background-disabled);
46
+ color: var(--anfra-color-text-disabled);
47
+ }
48
+ .a-button[aria-invalid="true"] { border-color: var(--anfra-color-border-danger-strong); }
49
+ .a-button:active { background-color: var(--anfra-color-background-highlight-active); }
50
+ @media (hover: hover) {
51
+ .a-button:hover { background-color: var(--anfra-color-background-highlight-hover); }
52
+ }
53
+
54
+ .a-button[data-variant="outline"] {
55
+ border-color: var(--anfra-color-border-strong);
56
+ background-color: var(--anfra-color-background-default);
57
+ color: var(--anfra-color-text-default);
58
+ }
59
+ @media (hover: hover) {
60
+ .a-button[data-variant="outline"]:hover { border-color: var(--anfra-color-border-hover); background-color: var(--anfra-color-background-default); }
61
+ }
62
+ .a-button[data-variant="outline"]:is(:active, [aria-expanded="true"], [aria-pressed="true"]) { background-color: var(--anfra-color-background-active); }
63
+
64
+ .a-button[data-variant="secondary"] {
65
+ background-color: var(--anfra-color-background-fill-default);
66
+ color: var(--anfra-color-text-default);
67
+ }
68
+ @media (hover: hover) {
69
+ .a-button[data-variant="secondary"]:hover { background-color: var(--anfra-color-background-fill-hover); }
70
+ }
71
+ .a-button[data-variant="secondary"]:is(:active, [aria-expanded="true"]) { background-color: var(--anfra-color-background-active); }
72
+
73
+ .a-button[data-variant="ghost"] { background-color: transparent; color: var(--anfra-color-text-weak); }
74
+ @media (hover: hover) {
75
+ .a-button[data-variant="ghost"]:hover { background-color: var(--anfra-color-background-hover); color: var(--anfra-color-text-default); }
76
+ }
77
+ .a-button[data-variant="ghost"]:active { background-color: var(--anfra-color-background-active); }
78
+ .a-button[data-variant="ghost"]:is([aria-expanded="true"], [aria-pressed="true"]) {
79
+ background-color: var(--anfra-color-background-active);
80
+ color: var(--anfra-color-text-default);
81
+ }
82
+ .a-button[data-variant="ghost"]:is(:disabled, [aria-disabled="true"]) { border-color: transparent; background-color: transparent; }
83
+
84
+ .a-button[data-variant="destructive"] {
85
+ background-color: var(--anfra-color-background-danger-default);
86
+ color: var(--anfra-color-text-on-fill);
87
+ }
88
+ @media (hover: hover) {
89
+ .a-button[data-variant="destructive"]:hover { background-color: var(--anfra-color-background-danger-hover); }
90
+ }
91
+ .a-button[data-variant="destructive"]:active { background-color: var(--anfra-color-background-danger-hover); }
92
+
93
+ .a-button[data-variant="link"] { background-color: transparent; color: var(--anfra-color-text-link-default); text-underline-offset: 4px; }
94
+ @media (hover: hover) {
95
+ .a-button[data-variant="link"]:hover { background-color: transparent; color: var(--anfra-color-text-link-hover); text-decoration: underline; }
96
+ }
97
+ .a-button[data-variant="link"]:active { background-color: transparent; }
98
+ .a-button[data-variant="link"]:is(:disabled, [aria-disabled="true"]) { border-color: transparent; background-color: transparent; }
99
+
100
+ .a-button[data-size="xs"] { gap: 4px; height: 24px; padding: 0 8px; font: var(--anfra-type-caption); font-weight: 500; }
101
+ .a-button[data-size="xs"] svg { width: 12px; height: 12px; }
102
+ .a-button[data-size="sm"] { gap: 4px; height: 28px; padding: 0 10px; font: var(--anfra-type-caption); font-weight: 500; }
103
+ .a-button[data-size="sm"] svg { width: 14px; height: 14px; }
104
+ .a-button[data-size="lg"] { height: 36px; }
105
+ .a-button:is([data-size="icon"], [data-size="icon-xs"], [data-size="icon-sm"], [data-size="icon-lg"]) { padding: 0; }
106
+ .a-button[data-size="icon"] { width: 32px; }
107
+ .a-button[data-size="icon-xs"] { width: 24px; height: 24px; }
108
+ .a-button[data-size="icon-xs"] svg { width: 12px; height: 12px; }
109
+ .a-button[data-size="icon-sm"] { width: 28px; height: 28px; }
110
+ .a-button[data-size="icon-lg"] { width: 36px; height: 36px; }
111
+ /* Under 32px, lg would read as a pill: small controls take md. */
112
+ .a-button:is([data-size="xs"], [data-size="sm"], [data-size="icon-xs"], [data-size="icon-sm"]) { border-radius: var(--anfra-radius-md); }
113
+
114
+ /* Input: shadcn's Input, edged as an outline button is. Focus shows on its own border, violet and
115
+ doubled, never an outline. A field with an icon is a label.a-input around the icon and a bare input. */
116
+ .a-input {
117
+ box-sizing: border-box;
118
+ width: 100%;
119
+ min-width: 0;
120
+ height: 32px;
121
+ padding: 4px 10px;
122
+ border: 1px solid var(--anfra-color-border-strong); /* the outline button's edge; darker on hover */
123
+ border-radius: var(--anfra-radius-lg);
124
+ background: transparent;
125
+ color: var(--anfra-color-text-default);
126
+ font: var(--anfra-type-body);
127
+ outline: none;
128
+ transition: border-color 160ms var(--anfra-ease-out);
129
+ }
130
+ .a-input::placeholder { color: var(--anfra-color-text-placeholder); }
131
+ @media (hover: hover) {
132
+ .a-input:hover { border-color: var(--anfra-color-border-hover); }
133
+ }
134
+ .a-input:is([readonly], :has(input[readonly])) { border-color: var(--anfra-color-border-strong); background: var(--anfra-color-background-secondary-default); }
135
+ .a-input:is(:disabled, :has(input:disabled)) {
136
+ cursor: not-allowed;
137
+ border-color: var(--anfra-color-border-disabled);
138
+ background: var(--anfra-color-background-disabled);
139
+ color: var(--anfra-color-text-disabled);
140
+ }
141
+ .a-input:is(:focus-visible, :has(:focus-visible)) {
142
+ border-color: var(--anfra-color-border-highlight-strong);
143
+ box-shadow: inset 0 0 0 1px var(--anfra-color-border-highlight-strong);
144
+ }
145
+ .a-input[aria-invalid="true"],
146
+ .a-input[aria-invalid="true"]:focus-visible {
147
+ border-color: var(--anfra-color-border-danger-strong);
148
+ }
149
+ .a-input[aria-invalid="true"]:focus-visible { box-shadow: inset 0 0 0 1px var(--anfra-color-border-danger-strong); }
150
+
151
+ /* Badge: shadcn's Badge, a label in a pill. Variants default (filled), secondary, outline,
152
+ destructive, warning, success. */
153
+ .a-badge {
154
+ display: inline-flex;
155
+ flex-shrink: 0;
156
+ align-items: center;
157
+ justify-content: center;
158
+ gap: 4px;
159
+ width: fit-content;
160
+ height: 20px;
161
+ padding: 2px 8px;
162
+ overflow: hidden;
163
+ border: 1px solid transparent;
164
+ border-radius: var(--anfra-radius-pill);
165
+ background: var(--anfra-color-background-highlight-default);
166
+ color: var(--anfra-color-text-on-fill);
167
+ font: var(--anfra-type-caption);
168
+ font-weight: 500;
169
+ white-space: nowrap;
170
+ box-sizing: border-box;
171
+ }
172
+ .a-badge svg { width: 12px; height: 12px; pointer-events: none; }
173
+ /* A badge holding one icon or glyph is square, as wide as it is tall, so its padding is even. */
174
+ .a-badge[data-size="icon"] { width: 20px; padding: 0; }
175
+ .a-badge[data-variant="secondary"] {
176
+ border-color: var(--anfra-color-border-default);
177
+ background: var(--anfra-color-background-info-secondary-default);
178
+ color: var(--anfra-color-text-info-default);
179
+ }
180
+ .a-badge[data-variant="outline"] { border-color: var(--anfra-color-border-strong); background: transparent; color: var(--anfra-color-text-default); }
181
+ .a-badge[data-variant="destructive"] {
182
+ border-color: var(--anfra-color-border-danger-default);
183
+ background: var(--anfra-color-background-danger-secondary-default);
184
+ color: var(--anfra-color-text-danger-default);
185
+ }
186
+ .a-badge[data-variant="warning"] {
187
+ border-color: var(--anfra-color-border-warning-default);
188
+ background: var(--anfra-color-background-warning-secondary-default);
189
+ color: var(--anfra-color-text-warning-default);
190
+ }
191
+ .a-badge[data-variant="success"] {
192
+ border-color: var(--anfra-color-border-success-default);
193
+ background: var(--anfra-color-background-success-secondary-default);
194
+ color: var(--anfra-color-text-success-default);
195
+ }
196
+
197
+ /* Status pill: Cloud's StatusPill, a state as a dot and a label. Tones neutral (the default),
198
+ highlight, success, warning, danger; the label always says it, never the colour alone. */
199
+ .a-pill {
200
+ display: inline-flex;
201
+ flex-shrink: 0;
202
+ align-items: center;
203
+ gap: 6px;
204
+ width: fit-content;
205
+ height: 20px;
206
+ padding: 0 8px;
207
+ border-radius: var(--anfra-radius-pill);
208
+ background: var(--anfra-color-background-active);
209
+ color: var(--anfra-color-text-weak);
210
+ font: var(--anfra-type-caption);
211
+ font-weight: 500;
212
+ white-space: nowrap;
213
+ }
214
+ .a-pill::before { content: ""; flex-shrink: 0; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
215
+ .a-pill[data-tone="highlight"] { background: var(--anfra-color-background-highlight-secondary-default); color: var(--anfra-color-text-highlight-default); }
216
+ .a-pill[data-tone="success"] { background: var(--anfra-color-background-success-secondary-default); color: var(--anfra-color-text-success-default); }
217
+ .a-pill[data-tone="warning"] { background: var(--anfra-color-background-warning-secondary-default); color: var(--anfra-color-text-warning-default); }
218
+ .a-pill[data-tone="danger"] { background: var(--anfra-color-background-danger-secondary-default); color: var(--anfra-color-text-danger-default); }
219
+
220
+ /* Tabs: shadcn's TabsList and TabsTrigger. A segmented list by default; data-variant="line" for an
221
+ underline. The open tab is aria-selected="true", or aria-pressed="true" in a group of toggles (a filter). */
222
+ .a-tabs {
223
+ display: inline-flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ width: fit-content;
227
+ height: 32px;
228
+ padding: 3px;
229
+ border-radius: var(--anfra-radius-lg);
230
+ background: var(--anfra-color-background-fill-default); /* a tint of its ground, so it reads on the panel and in a well */
231
+ color: var(--anfra-color-text-weak);
232
+ box-sizing: border-box;
233
+ }
234
+ /* Small: a 24px list for a toggle in a dense row, its track 2px. */
235
+ .a-tabs[data-size="sm"] {
236
+ height: 24px;
237
+ padding: 2px;
238
+ border-radius: var(--anfra-radius-md); /* under 32px, a control takes md */
239
+ }
240
+ .a-tabs[data-size="sm"] .a-tab {
241
+ padding: 0 6px;
242
+ border-radius: calc(var(--anfra-radius-md) - 2px); /* nested in the list: its radius less its padding */
243
+ font: var(--anfra-type-caption);
244
+ font-weight: 500;
245
+ }
246
+ .a-tab {
247
+ position: relative;
248
+ box-sizing: border-box;
249
+ display: inline-flex;
250
+ flex: 1;
251
+ align-items: center;
252
+ justify-content: center;
253
+ gap: 6px;
254
+ height: 100%; /* shadcn's is 1px short, which leaves the track 3.5px above and below and 3px at the sides */
255
+ padding: 2px 6px;
256
+ border: 1px solid transparent;
257
+ border-radius: calc(var(--anfra-radius-lg) - 3px); /* nested in the list: its radius less its padding */
258
+ background: transparent;
259
+ color: var(--anfra-color-text-weak);
260
+ font: var(--anfra-type-label);
261
+ white-space: nowrap;
262
+ cursor: pointer;
263
+ }
264
+ .a-tab svg { flex-shrink: 0; width: 16px; height: 16px; pointer-events: none; }
265
+ @media (hover: hover) {
266
+ .a-tab:hover { color: var(--anfra-color-text-default); }
267
+ }
268
+ .a-tab:is(:disabled, [aria-disabled="true"]) { pointer-events: none; color: var(--anfra-color-text-disabled); }
269
+ .a-tab:is([aria-selected="true"], [aria-pressed="true"]) {
270
+ background: var(--anfra-color-background-fill-selected);
271
+ color: var(--anfra-color-text-default);
272
+ box-shadow: var(--anfra-e1);
273
+ }
274
+ .a-tabs[data-variant="line"] { border-radius: 0; background: transparent; }
275
+ .a-tabs[data-variant="line"] .a-tab:is([aria-selected="true"], [aria-pressed="true"]) { background: transparent; box-shadow: none; }
276
+ .a-tabs[data-variant="line"] .a-tab::after {
277
+ content: "";
278
+ position: absolute;
279
+ inset: auto 0 -5px;
280
+ height: 2px;
281
+ background: var(--anfra-color-background-inverted-default);
282
+ opacity: 0;
283
+ }
284
+ .a-tabs[data-variant="line"] .a-tab:is([aria-selected="true"], [aria-pressed="true"])::after { opacity: 1; }
285
+
286
+ /* Menu: shadcn's DropdownMenuContent and its parts. An item is highlighted on hover, on focus, or
287
+ with data-highlighted (a menu that moves a highlight by keyboard). */
288
+ .a-menu {
289
+ box-sizing: border-box;
290
+ min-width: 128px;
291
+ padding: 4px;
292
+ overflow: hidden auto;
293
+ border-radius: var(--anfra-radius-xl);
294
+ background: var(--anfra-color-background-raised-default);
295
+ color: var(--anfra-color-text-default);
296
+ box-shadow: var(--anfra-e2); /* its edge is e2's ring, not a border */
297
+ }
298
+ .a-menu-item {
299
+ position: relative;
300
+ display: flex;
301
+ align-items: center;
302
+ gap: 6px;
303
+ width: 100%;
304
+ padding: 4px 6px;
305
+ border: 0;
306
+ border-radius: calc(var(--anfra-radius-xl) - 4px); /* nested in the menu: its radius less its padding */
307
+ background: transparent;
308
+ color: inherit;
309
+ font: var(--anfra-type-body);
310
+ text-align: left;
311
+ cursor: default;
312
+ user-select: none;
313
+ outline: none;
314
+ box-sizing: border-box;
315
+ }
316
+ .a-menu-item svg { flex-shrink: 0; width: 16px; height: 16px; pointer-events: none; }
317
+ .a-menu-item:is(:focus-visible, [data-highlighted]) { background: var(--anfra-color-background-hover); color: var(--anfra-color-text-default); }
318
+ @media (hover: hover) {
319
+ .a-menu-item:hover { background: var(--anfra-color-background-hover); color: var(--anfra-color-text-default); }
320
+ }
321
+ .a-menu-item:is(:disabled, [aria-disabled="true"]) { pointer-events: none; color: var(--anfra-color-text-disabled); }
322
+ .a-menu-item:is(:disabled, [aria-disabled="true"]) svg { color: var(--anfra-color-icon-disabled); }
323
+ .a-menu-item[data-variant="destructive"],
324
+ .a-menu-item[data-variant="destructive"] svg { color: var(--anfra-color-text-danger-default); }
325
+ .a-menu-item[data-variant="destructive"]:is(:focus-visible, [data-highlighted]) { background: var(--anfra-color-background-danger-secondary-default); }
326
+ @media (hover: hover) {
327
+ .a-menu-item[data-variant="destructive"]:hover { background: var(--anfra-color-background-danger-secondary-default); }
328
+ }
329
+ .a-menu-label { padding: 4px 6px; color: var(--anfra-color-text-placeholder); font: var(--anfra-type-caption); font-weight: 500; }
330
+ .a-menu-separator { height: 1px; margin: 4px -4px; border: 0; background: var(--anfra-color-border-default); }
331
+ .a-menu-shortcut { margin-left: auto; color: var(--anfra-color-text-placeholder); font: var(--anfra-type-caption); letter-spacing: 0.1em; }
332
+
333
+ /* Tooltip: shadcn's TooltipContent, inverted. */
334
+ .a-tooltip {
335
+ box-sizing: border-box;
336
+ display: inline-flex;
337
+ align-items: center;
338
+ gap: 6px;
339
+ width: fit-content;
340
+ max-width: 320px;
341
+ padding: 6px 12px;
342
+ border-radius: var(--anfra-radius-md);
343
+ background: var(--anfra-color-background-inverted-default);
344
+ color: var(--anfra-color-text-inverted-default);
345
+ font: var(--anfra-type-caption);
346
+ }
347
+
348
+ /* Kbd: a key, as a menu's shortcut or a field's hint shows it. */
349
+ .a-kbd {
350
+ display: inline-flex;
351
+ align-items: center;
352
+ justify-content: center;
353
+ min-width: 20px;
354
+ height: 20px;
355
+ padding: 0 4px;
356
+ border-radius: var(--anfra-radius-md);
357
+ background: var(--anfra-color-background-active);
358
+ color: var(--anfra-color-text-weak);
359
+ font: var(--anfra-type-caption);
360
+ font-weight: 500;
361
+ box-sizing: border-box;
362
+ pointer-events: none;
363
+ user-select: none;
364
+ }
365
+ /* KbdGroup: a combination, one Kbd per key. */
366
+ .a-kbd-group { display: inline-flex; flex: none; align-items: center; gap: 4px; }
367
+
368
+ /* Sidebar: shadcn's SidebarGroupLabel, SidebarMenuButton (.a-nav-item), SidebarMenuSub (.a-tree) and
369
+ SidebarMenuSubButton (.a-tree-row). The current page is aria-current="page" (or data-active): a
370
+ raised chip with a hairline, and a violet icon. */
371
+ .a-nav-label {
372
+ display: flex;
373
+ flex-shrink: 0;
374
+ align-items: center;
375
+ height: 32px;
376
+ padding: 0 8px;
377
+ border-radius: var(--anfra-radius-md);
378
+ color: var(--anfra-color-text-placeholder);
379
+ font: var(--anfra-type-caption);
380
+ font-weight: 500;
381
+ }
382
+ .a-nav-item,
383
+ .a-tree-row {
384
+ box-sizing: border-box;
385
+ display: flex;
386
+ align-items: center;
387
+ gap: 8px;
388
+ width: 100%;
389
+ min-width: 0;
390
+ overflow: hidden;
391
+ border: 0;
392
+ border-radius: var(--anfra-radius-md);
393
+ background: transparent;
394
+ color: var(--anfra-color-text-weak);
395
+ font: var(--anfra-type-body);
396
+ text-align: left;
397
+ text-decoration: none;
398
+ cursor: pointer;
399
+ }
400
+ .a-nav-item { height: 32px; padding: 8px; }
401
+ .a-nav-item[data-size="sm"] { height: 28px; font: var(--anfra-type-caption); }
402
+ .a-tree-row { height: 28px; padding: 0 8px; }
403
+ .a-nav-item > :last-child,
404
+ .a-tree-row > :last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
405
+ .a-nav-item svg,
406
+ .a-tree-row svg { flex-shrink: 0; width: 16px; height: 16px; color: var(--anfra-color-icon-weak); }
407
+ @media (hover: hover) {
408
+ .a-nav-item:hover,
409
+ .a-tree-row:hover { background: var(--anfra-color-background-hover); color: var(--anfra-color-text-default); }
410
+ }
411
+ .a-nav-item:active,
412
+ .a-tree-row:active { background: var(--anfra-color-background-active); color: var(--anfra-color-text-default); }
413
+ .a-nav-item:is(:disabled, [aria-disabled="true"]),
414
+ .a-tree-row:is(:disabled, [aria-disabled="true"]) { pointer-events: none; color: var(--anfra-color-text-disabled); }
415
+ .a-nav-item:is([aria-current="page"], [data-active]),
416
+ .a-tree-row:is([aria-current="page"], [data-active]) {
417
+ background: var(--anfra-color-background-current-default);
418
+ color: var(--anfra-color-text-default);
419
+ box-shadow: inset 0 0 0 1px var(--anfra-color-border-weak);
420
+ }
421
+ .a-nav-item:is([aria-current="page"], [data-active]) { font-weight: 500; }
422
+ .a-nav-item:is([aria-current="page"], [data-active]) svg,
423
+ .a-tree-row:is([aria-current="page"], [data-active]) svg { color: var(--anfra-color-icon-highlight-default); }
424
+ .a-tree {
425
+ display: flex;
426
+ flex-direction: column;
427
+ gap: 4px;
428
+ min-width: 0;
429
+ margin: 0 14px;
430
+ padding: 2px 10px;
431
+ border-left: 1px solid var(--anfra-color-border-default);
432
+ translate: 1px 0;
433
+ }
434
+ .a-tree .a-tree-row { translate: -1px 0; }
435
+
436
+ /* Focus: one 2px violet outline, offset 3px, on every control here but a field and a menu item. */
437
+ :is(.a-button, .a-tab, .a-nav-item, .a-tree-row):focus-visible {
438
+ outline: 2px solid var(--anfra-color-focus-ring-default);
439
+ outline-offset: 3px;
440
+ }
package/icons.d.ts ADDED
@@ -0,0 +1,59 @@
1
+ // Generated by scripts/icons.mjs from lucide-static 1.63.0 (ISC). Do not edit.
2
+
3
+ export type IconName =
4
+ | "folder"
5
+ | "folder-open"
6
+ | "app"
7
+ | "database"
8
+ | "dataset"
9
+ | "model"
10
+ | "metric"
11
+ | "dimension"
12
+ | "block"
13
+ | "control"
14
+ | "query"
15
+ | "search"
16
+ | "reload"
17
+ | "update"
18
+ | "copy"
19
+ | "check"
20
+ | "download"
21
+ | "close"
22
+ | "plus"
23
+ | "minus"
24
+ | "filter"
25
+ | "pick"
26
+ | "locate"
27
+ | "inspect"
28
+ | "focus"
29
+ | "expand"
30
+ | "fit"
31
+ | "eye"
32
+ | "hidden"
33
+ | "chevron"
34
+ | "sidebar"
35
+ | "more"
36
+ | "overflow"
37
+ | "command"
38
+ | "sun"
39
+ | "moon"
40
+ | "alert"
41
+ | "info"
42
+ | "help"
43
+ | "overview"
44
+ | "table"
45
+ | "sheet"
46
+ | "lineage"
47
+ | "tree"
48
+ | "code"
49
+ | "type-text"
50
+ | "type-number"
51
+ | "type-date"
52
+ | "type-datetime"
53
+ | "type-truefalse"
54
+ | "type-bit"
55
+ | "type-json"
56
+ | "type-unknown";
57
+
58
+ /** anfra's icons, by name: path data on a 24px grid, drawn with a 2px round stroke and no fill. */
59
+ export declare const ICONS: Readonly<Record<IconName, string>>;
package/icons.js ADDED
@@ -0,0 +1,58 @@
1
+ // Generated by scripts/icons.mjs from lucide-static 1.63.0 (ISC). Do not edit.
2
+
3
+ /** anfra's icons, by name: path data on a 24px grid, drawn with a 2px round stroke and no fill. */
4
+ export const ICONS = {
5
+ "folder": "M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",
6
+ "folder-open": "M6 14l1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",
7
+ "app": "M4 3h5a1 1 0 0 1 1 1v7a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1v-7a1 1 0 0 1 1 -1ZM15 3h5a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1v-3a1 1 0 0 1 1 -1ZM15 12h5a1 1 0 0 1 1 1v7a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1v-7a1 1 0 0 1 1 -1ZM4 16h5a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1v-3a1 1 0 0 1 1 -1Z",
8
+ "database": "M3 5a9 3 0 1 0 18 0a9 3 0 1 0 -18 0M3 5V19A9 3 0 0 0 21 19V5M3 12A9 3 0 0 0 21 12",
9
+ "dataset": "M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83zM2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17",
10
+ "model": "M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16ZM3.3 7l8.7 5 8.7-5M12 22V12",
11
+ "metric": "M18 7V5a1 1 0 0 0-1-1H6.5a.5.5 0 0 0-.4.8l4.5 6a2 2 0 0 1 0 2.4l-4.5 6a.5.5 0 0 0 .4.8H17a1 1 0 0 0 1-1v-2",
12
+ "dimension": "M21.3 15.3a2.4 2.4 0 0 1 0 3.4l-2.6 2.6a2.4 2.4 0 0 1-3.4 0L2.7 8.7a2.41 2.41 0 0 1 0-3.4l2.6-2.6a2.41 2.41 0 0 1 3.4 0ZM14.5 12.5l2-2M11.5 9.5l2-2M8.5 6.5l2-2M17.5 15.5l2-2",
13
+ "block": "M4 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1v-5a1 1 0 0 1 1 -1ZM15 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1v-5a1 1 0 0 1 1 -1ZM15 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1v-5a1 1 0 0 1 1 -1ZM4 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1v-5a1 1 0 0 1 1 -1Z",
14
+ "control": "M10 5H3M12 19H3M14 3v4M16 17v4M21 12h-9M21 19h-5M21 5h-7M8 10v4M8 12H3",
15
+ "query": "M3 5a9 3 0 1 0 18 0a9 3 0 1 0 -18 0M3 5V19A9 3 0 0 0 21 19V5M3 12A9 3 0 0 0 21 12",
16
+ "search": "M21 21l-4.34-4.34M3 11a8 8 0 1 0 16 0a8 8 0 1 0 -16 0",
17
+ "reload": "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8M21 3v5h-5M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16M8 16H3v5",
18
+ "update": "M5 12l7-7 7 7M12 19V5",
19
+ "copy": "M10 8h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2ZM4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",
20
+ "check": "M20 6 9 17l-5-5",
21
+ "download": "M12 15V3M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5",
22
+ "close": "M18 6 6 18M6 6l12 12",
23
+ "plus": "M5 12h14M12 5v14",
24
+ "minus": "M5 12h14",
25
+ "filter": "M2 5h20M6 12h12M9 19h6",
26
+ "pick": "M2 12a10 10 0 1 0 20 0a10 10 0 1 0 -20 0M22 12L18 12M6 12L2 12M12 6L12 2M12 22L12 18",
27
+ "locate": "M2 12L5 12M19 12L22 12M12 2L12 5M12 19L12 22M5 12a7 7 0 1 0 14 0a7 7 0 1 0 -14 0M9 12a3 3 0 1 0 6 0a3 3 0 1 0 -6 0",
28
+ "inspect": "M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2ZM15 3v18",
29
+ "focus": "M9 12a3 3 0 1 0 6 0a3 3 0 1 0 -6 0M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2",
30
+ "expand": "M15 3h6v6M21 3l-7 7M3 21l7-7M9 21H3v-6",
31
+ "fit": "M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2",
32
+ "eye": "M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0M9 12a3 3 0 1 0 6 0a3 3 0 1 0 -6 0",
33
+ "hidden": "M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49M14.084 14.158a3 3 0 0 1-4.242-4.242M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143M2 2l20 20",
34
+ "chevron": "M9 18l6-6-6-6",
35
+ "sidebar": "M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2ZM9 3v18",
36
+ "more": "M11 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M18 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M4 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0",
37
+ "overflow": "M6 17l5-5-5-5M13 17l5-5-5-5",
38
+ "command": "M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3",
39
+ "sun": "M8 12a4 4 0 1 0 8 0a4 4 0 1 0 -8 0M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41",
40
+ "moon": "M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401",
41
+ "alert": "M21.73 18l-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3M12 9v4M12 17h.01",
42
+ "info": "M2 12a10 10 0 1 0 20 0a10 10 0 1 0 -20 0M12 16v-4M12 8h.01",
43
+ "help": "M2 12a10 10 0 1 0 20 0a10 10 0 1 0 -20 0M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3M12 17h.01",
44
+ "overview": "M4 3h5a1 1 0 0 1 1 1v7a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1v-7a1 1 0 0 1 1 -1ZM15 3h5a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1v-3a1 1 0 0 1 1 -1ZM15 12h5a1 1 0 0 1 1 1v7a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1v-7a1 1 0 0 1 1 -1ZM4 16h5a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1v-3a1 1 0 0 1 1 -1Z",
45
+ "table": "M12 3v18M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2ZM3 9h18M3 15h18",
46
+ "sheet": "M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2ZM3 9L21 9M3 15L21 15M9 9L9 21M15 9L15 21",
47
+ "lineage": "M9 18a3 3 0 1 0 6 0a3 3 0 1 0 -6 0M3 6a3 3 0 1 0 6 0a3 3 0 1 0 -6 0M15 6a3 3 0 1 0 6 0a3 3 0 1 0 -6 0M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9M12 12v3",
48
+ "tree": "M8 5h13M13 12h8M13 19h8M3 10a2 2 0 0 0 2 2h3M3 5v12a2 2 0 0 0 2 2h3",
49
+ "code": "M16 18l6-6-6-6M8 6l-6 6 6 6",
50
+ "type-text": "M12 4v16M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2M9 20h6",
51
+ "type-number": "M4 9L20 9M4 15L20 15M10 3L8 21M16 3L14 21",
52
+ "type-date": "M8 2v3M16 2v3M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2ZM3 9h18",
53
+ "type-datetime": "M16 14v2.2l1.6 1M16 2v3M21 7.338V5a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2h2.338M3 9h5.859M8 2v3M10 16a6 6 0 1 0 12 0a6 6 0 1 0 -12 0",
54
+ "type-truefalse": "M6 12a3 3 0 1 0 6 0a3 3 0 1 0 -6 0M9 5h6a7 7 0 0 1 7 7v0a7 7 0 0 1 -7 7h-6a7 7 0 0 1 -7 -7v0a7 7 0 0 1 7 -7Z",
55
+ "type-bit": "M16 14h0a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h0a2 2 0 0 1 -2 -2v-2a2 2 0 0 1 2 -2ZM8 4h0a2 2 0 0 1 2 2v2a2 2 0 0 1 -2 2h0a2 2 0 0 1 -2 -2v-2a2 2 0 0 1 2 -2ZM6 20h4M14 10h4M6 14h2v6M14 4h2v6",
56
+ "type-json": "M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1",
57
+ "type-unknown": "M10.1 2.182a10 10 0 0 1 3.8 0M13.9 21.818a10 10 0 0 1-3.8 0M17.609 3.721a10 10 0 0 1 2.69 2.7M2.182 13.9a10 10 0 0 1 0-3.8M20.279 17.609a10 10 0 0 1-2.7 2.69M21.818 10.1a10 10 0 0 1 0 3.8M3.721 6.391a10 10 0 0 1 2.7-2.69M6.391 20.279a10 10 0 0 1-2.69-2.7",
58
+ };
package/index.css CHANGED
@@ -1,4 +1,5 @@
1
- /* Everything a host needs: the fonts, the tokens and the type roles. */
1
+ /* Everything a host needs: the fonts, the tokens, the type roles and the class layer. */
2
2
  @import './fonts.css';
3
3
  @import './tokens.css';
4
4
  @import './type.css';
5
+ @import './components.css';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holistics/anfra-design",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "anfra's design tokens, type and rules: one design language for anfra core (appserve, the SDK's plugin UI) and anfra Cloud",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -15,6 +15,11 @@
15
15
  "./tokens.css": "./tokens.css",
16
16
  "./fonts.css": "./fonts.css",
17
17
  "./type.css": "./type.css",
18
+ "./components.css": "./components.css",
19
+ "./icons": {
20
+ "types": "./icons.d.ts",
21
+ "default": "./icons.js"
22
+ },
18
23
  "./DESIGN.md": "./DESIGN.md",
19
24
  "./package.json": "./package.json"
20
25
  },
@@ -23,13 +28,20 @@
23
28
  "tokens.css",
24
29
  "fonts.css",
25
30
  "type.css",
31
+ "components.css",
32
+ "icons.js",
33
+ "icons.d.ts",
26
34
  "DESIGN.md"
27
35
  ],
28
36
  "scripts": {
29
- "test": "node --test"
37
+ "test": "node --test",
38
+ "icons": "node scripts/icons.mjs"
30
39
  },
31
40
  "dependencies": {
32
41
  "@fontsource-variable/geist-mono": "^5.3.0",
33
42
  "@fontsource-variable/inter": "^5.3.0"
43
+ },
44
+ "devDependencies": {
45
+ "lucide-static": "1.63.0"
34
46
  }
35
47
  }
package/tokens.css CHANGED
@@ -47,6 +47,7 @@
47
47
  --anfra-paper-100: oklch(0.955 0.007 88.6); /* #f2f0eb */
48
48
  --anfra-paper-200: oklch(0.925 0.01 87.5); /* #e9e6df */
49
49
 
50
+ --anfra-violet-50: oklch(0.955 0.025 298.6); /* #f2edff the logomark's inverse, on dark */
50
51
  --anfra-violet-100: oklch(0.946 0.027 304.8); /* #f1e9fc */
51
52
  --anfra-violet-200: oklch(0.904 0.049 303.6); /* #e6d8fb */
52
53
  --anfra-violet-300: oklch(0.878 0.072 302.8); /* #e0ccff */
@@ -124,6 +125,13 @@
124
125
  --anfra-ink-a5
125
126
  ); /* Neutral hover layer (works on paper and on white) */
126
127
  --anfra-color-background-active: var(--anfra-ink-a9); /* Neutral pressed layer; open trigger */
128
+ --anfra-color-background-fill-default: var(
129
+ --anfra-ink-a4
130
+ ); /* A control's rest fill (secondary button, tab track): a tint of its ground, on white or in a well */
131
+ --anfra-color-background-fill-hover: var(--anfra-ink-a8); /* Hover on a filled control */
132
+ --anfra-color-background-fill-selected: var(
133
+ --anfra-ink-0
134
+ ); /* The open tab, raised off its track */
127
135
  --anfra-color-background-disabled: var(
128
136
  --anfra-ink-a4
129
137
  ); /* Disabled control fill (all variants, never violet) */
@@ -196,6 +204,7 @@
196
204
  --anfra-color-icon-highlight-default: var(
197
205
  --anfra-violet-600
198
206
  ); /* Current nav icon, checked mark */
207
+ --anfra-color-icon-logo: var(--anfra-violet-600); /* The logomark: the artwork's violet */
199
208
  --anfra-color-icon-on-fill: var(--anfra-ink-0); /* Icon on solid fills */
200
209
  --anfra-color-icon-inverted-default: var(--anfra-ink-100); /* Icon on inverted bg */
201
210
  --anfra-color-icon-danger-default: var(--anfra-red-500); /* Danger icon */
@@ -227,10 +236,12 @@
227
236
  --anfra-color-focus-ring-default: var(--anfra-violet-600); /* 2px focus outline, offset 3px */
228
237
 
229
238
  /* Elevation (light): shadows. */
230
- --anfra-e1: 0 1px 0 rgb(24 20 31 / 0.04), 0 1px 2px rgb(24 20 31 / 0.06);
239
+ --anfra-e1: 0 1px 0 oklch(0.201 0.022 301.1 / 0.04), 0 1px 2px oklch(0.201 0.022 301.1 / 0.06);
240
+ /* A floating surface (menu, popover): a translucent 1px ring for its edge, so one token reads on
241
+ any ground, then a tight layer and a soft one for its lift. */
231
242
  --anfra-e2:
232
- 0 1px 0 rgb(24 20 31 / 0.03), 0 18px 40px -18px rgb(24 20 31 / 0.28),
233
- 0 2px 6px -2px rgb(24 20 31 / 0.08);
243
+ 0 0 0 1px oklch(0.201 0.022 301.1 / 0.08), 0 1px 3px oklch(0.201 0.022 301.1 / 0.06),
244
+ 0 18px 40px -18px oklch(0.201 0.022 301.1 / 0.28), 0 2px 6px -2px oklch(0.201 0.022 301.1 / 0.08);
234
245
 
235
246
  /* Motion (DESIGN.md §10). Unchanged. */
236
247
  --anfra-t-press: 90ms;
@@ -257,6 +268,11 @@
257
268
  ); /* was ink-875 (= nav hover color) */
258
269
  --anfra-color-background-hover: var(--anfra-white-a6);
259
270
  --anfra-color-background-active: var(--anfra-white-a10);
271
+ /* Not a swap of the light values: on dark a control is lighter than its ground, and the open tab
272
+ lighter than its track, since lighter reads as closer. White layers lift from any ground. */
273
+ --anfra-color-background-fill-default: var(--anfra-white-a4);
274
+ --anfra-color-background-fill-hover: var(--anfra-white-a8);
275
+ --anfra-color-background-fill-selected: var(--anfra-white-a8); /* on the track's a4: two steps up */
260
276
  --anfra-color-background-disabled: var(--anfra-white-a4);
261
277
  --anfra-color-background-inverted-default: var(--anfra-ink-100);
262
278
  --anfra-color-background-interactive-highlight-default: var(--anfra-violet-a18);
@@ -290,6 +306,7 @@
290
306
  --anfra-color-icon-hover: var(--anfra-ink-100);
291
307
  --anfra-color-icon-disabled: var(--anfra-ink-500); /* was ink-600 (exempt) */
292
308
  --anfra-color-icon-highlight-default: var(--anfra-violet-500);
309
+ --anfra-color-icon-logo: var(--anfra-violet-50); /* The logomark's inverse */
293
310
  --anfra-color-icon-inverted-default: var(--anfra-ink-900);
294
311
  --anfra-color-icon-danger-default: var(--anfra-red-400);
295
312
  --anfra-color-icon-warning-default: var(--anfra-amber-400);
@@ -313,7 +330,8 @@
313
330
 
314
331
  --anfra-color-focus-ring-default: var(--anfra-violet-500);
315
332
 
316
- /* A shadow does not show on dark: lift with a border instead (DESIGN.md §6). */
333
+ /* A shadow does not show on dark: lift with a ring instead (DESIGN.md §6). e2's is translucent
334
+ white, so it reads the same on every dark ground. */
317
335
  --anfra-e1: 0 0 0 1px var(--anfra-color-border-default);
318
- --anfra-e2: 0 0 0 1px var(--anfra-color-border-strong);
336
+ --anfra-e2: 0 0 0 1px var(--anfra-white-a10);
319
337
  }
package/type.css CHANGED
@@ -12,7 +12,7 @@
12
12
  --anfra-type-mono-label: 400 0.75rem/1rem var(--anfra-font-mono);
13
13
 
14
14
  /* Shapes: the smaller the thing, the smaller the corner. */
15
- --anfra-radius-md: 6px; /* menu items, nav items, chips, tooltips */
15
+ --anfra-radius-md: 6px; /* nav items, chips, tooltips, controls under 32px */
16
16
  --anfra-radius-lg: 10px; /* buttons, fields, alerts */
17
17
  --anfra-radius-xl: 14px; /* cards, popovers, dialogs, the panel */
18
18
  --anfra-radius-pill: 9999px; /* status pills and counters only */