@apliteni/apliteni-ui 0.11.1 → 0.12.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
@@ -31,6 +31,12 @@ as the vanilla kit, so the two layers can't drift.
31
31
  **Which one:** does the surface hold meaningful client state? No → the HTML-string
32
32
  factories below. Yes → the [React components](#react-components-stateful-surfaces).
33
33
 
34
+ Either layer follows the same UI rules — which component to reach for, the states it
35
+ owes, how colour and wording work. They live in the **Guidelines** section of Storybook,
36
+ which opens on
37
+ [an overview of the five pages](https://ui.apli.tech/storybook/?path=/story/guidelines-overview--overview)
38
+ and what the kit does and does not yet meet. Worth reading before you design a screen.
39
+
34
40
  ## Install
35
41
 
36
42
  Published on the **public npm registry** — no scope config, no token:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apliteni/apliteni-ui",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "workspaces": [
5
5
  "react"
6
6
  ],
@@ -59,7 +59,6 @@
59
59
  border-radius: 4px;
60
60
  }
61
61
  .rx-caret {
62
- opacity: .5;
63
62
  margin-left: 2px;
64
63
  }
65
64
  .rx-pager {
@@ -6,7 +6,29 @@
6
6
  //
7
7
  // Each value is the INNER markup; icon() wraps it in the shared <svg>. Glyphs
8
8
  // are grouped by domain — the flat ICONS map is what icon() looks up, and
9
- // iconCategories drives the Storybook grid. Add a glyph to the right group.
9
+ // iconCategories drives the Storybook grid.
10
+ //
11
+ // ---- Adding a glyph -------------------------------------------------------
12
+ //
13
+ // NAME it for what it depicts, not for the one place it is used: `trash`, not
14
+ // `deleteWorkspace`. camelCase, and a modifier follows its noun — `circleX`,
15
+ // `eyeOff`, `trendingUp` — so the family sorts together. A name is taken once;
16
+ // a second declaration of one is a gate failure, not a merge conflict.
17
+ //
18
+ // GROUP it by what it depicts, again rather than by caller. `chart` lives in
19
+ // DATA because it draws data, even when a comms panel is what renders it. If
20
+ // two groups both look right, the glyph belongs to the one whose other members
21
+ // it would sit beside in the catalogue. Groups are not tags: exactly one.
22
+ //
23
+ // PROVENANCE: the path comes from Lucide, unmodified, at the 24×24 / 1.7 house
24
+ // stroke — that is what keeps the set looking like one hand. Say which Lucide
25
+ // name it came from in the commit if the two differ. A hand-drawn path needs a
26
+ // reason in the commit message, because the next person cannot tell one from a
27
+ // traced one by looking.
28
+ //
29
+ // The gates: src/assets/icons.test.js holds one-group-per-glyph and the
30
+ // emitter's numbers, and stories/guidelines/iconography.test.js holds the
31
+ // icon-only list below. The written rules are on Guidelines / Iconography.
10
32
 
11
33
  const NAV = {
12
34
  chevronDown: '<path d="M6 9l6 6 6-6" stroke-linecap="round" stroke-linejoin="round"/>',
@@ -101,9 +123,6 @@ const COMMS = {
101
123
  logout: '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>',
102
124
  plug: '<path d="M12 22v-5"/><path d="M15 8V2"/><path d="M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z"/><path d="M9 8V2"/>',
103
125
  sparkle: '<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/>',
104
- card: '<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/>',
105
- doc: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>',
106
- chart: '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>',
107
126
  };
108
127
 
109
128
  const BRAND = {
@@ -140,5 +159,49 @@ export const icon = (name, cls = '') =>
140
159
 
141
160
  export const iconNames = Object.keys(ICONS);
142
161
 
162
+ // Which controls may be an icon and nothing else — a closed list, not a
163
+ // judgement call in review.
164
+ //
165
+ // The accessibility half was already held: every glyph is aria-hidden, so
166
+ // button({ iconOnly }) names itself from `label`. That says a nameless icon
167
+ // button cannot ship; it never said when a wordless one SHOULD. This does.
168
+ //
169
+ // A control whose action is on this list may drop its visible text. Everything
170
+ // else carries a label — including glyphs that feel obvious in isolation, like
171
+ // `gear` or `externalLink`, because a reader meets them one at a time and a
172
+ // toolbar is not a legend. Adding an entry is a decision recorded here, which
173
+ // is the point: the alternative rules read well and could not be gated.
174
+ export const iconOnlyAllowed = {
175
+ x: 'close or dismiss',
176
+ copy: 'copy to clipboard',
177
+ moreHorizontal: 'overflow menu',
178
+ moreVertical: 'overflow menu',
179
+ chevronDown: 'expand or collapse',
180
+ chevronUp: 'expand or collapse',
181
+ };
182
+
183
+ // What a glyph means when a component picks it for the reader rather than a
184
+ // caller naming it. Two rules hold the map together:
185
+ //
186
+ // a CIRCLED glyph is a state the system is in — circleCheck, circleX,
187
+ // circleAlert. It is reported to you and you cannot click it.
188
+ // a BARE glyph is an action you can take — x dismisses, check confirms,
189
+ // trash deletes.
190
+ //
191
+ // That split is what a danger toast needed: it used to render the same bare `x`
192
+ // twice, once meaning "this failed" and once meaning "make this go away". The
193
+ // bare x is now reserved for the close button, and status took the circle
194
+ // family the kit was already shipping and never using.
195
+ export const iconMeanings = {
196
+ circleCheck: 'a state: it succeeded',
197
+ circleX: 'a state: it failed',
198
+ circleAlert: 'a state: it needs attention',
199
+ info: 'a state: something worth knowing, no action required',
200
+ bolt: 'a state: it happened, with no verdict attached',
201
+ x: 'an action: close or dismiss this',
202
+ check: 'an action: confirm this',
203
+ trash: 'an action: delete this',
204
+ };
205
+
143
206
  export const sun = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>';
144
207
  export const moon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>';
@@ -16,6 +16,10 @@ export const esc = (s) => String(s == null ? '' : s).replace(/[&<>"]/g, (c) => (
16
16
  // stops being decoration and becomes the control's ONLY accessible name. It is
17
17
  // mirrored into aria-label + title, and an empty label falls back to the icon
18
18
  // name rather than shipping a nameless button.
19
+ //
20
+ // That is the accessibility half, and it is not permission. WHEN a control may
21
+ // go wordless is a closed list — `iconOnlyAllowed` in src/assets/icons.js — and
22
+ // stories/guidelines/iconography.test.js reviews call sites against it.
19
23
  export function button({
20
24
  label = 'Button', variant = 'secondary', size = 'md', icon: ic, iconSvg, iconRight,
21
25
  block = false, disabled = false, busy = false, type = 'button', href, iconOnly = false,
@@ -93,8 +97,14 @@ export function segmented({ options = [], active = 0, size, block, name = 'seg',
93
97
  }
94
98
 
95
99
  // ---- Accent picker -------------------------------------------------------
100
+ // Each swatch is made of the tokens its accent selects: it fades from the next
101
+ // distinct step up that accent's dark ramp down to dark --accent. Most accents
102
+ // land on --purple-light; Nebula's is its own --accent since #157, so it walks
103
+ // on to --purple-mid. site/chrome.mjs and site/index.html hand-keep the same
104
+ // four strings and must change with these. Held by stories/accent-swatch.test.js,
105
+ // which derives both sides rather than restating them. See issue #190.
96
106
  const ACCENT_SWATCH = {
97
- default: 'linear-gradient(135deg,#9b5dff,#6a2dcc)',
107
+ default: 'linear-gradient(135deg,#bd8cff,#b479ff)',
98
108
  phoenix: 'linear-gradient(135deg,#ff8a5c,#ff6a3d)',
99
109
  ocean: 'linear-gradient(135deg,#5ab0ff,#3b9dff)',
100
110
  emerald: 'linear-gradient(135deg,#3ad9a0,#16c98a)',
@@ -211,8 +221,18 @@ export function switchToggle({ checked = false, disabled = false, name, label =
211
221
  export function callout({ variant, icon: ic = 'info', body } = {}) {
212
222
  return `<div class="${cx('ui-callout', variant && `ui-callout--${variant}`)}"><span class="ui-callout__icon">${icon(ic)}</span><div>${body}</div></div>`;
213
223
  }
214
- // Default icon per status — overridable with `icon`.
215
- const TOAST_ICON = { success: 'check', danger: 'x', warn: 'alert', info: 'info', neutral: 'bolt' };
224
+ // Default icon per status — overridable with `icon`. The circle family is not
225
+ // decoration: a circled glyph is a STATE the system reports, a bare one is an
226
+ // ACTION you can take (iconMeanings in src/assets/icons.js).
227
+ //
228
+ // This used to read `success: 'check', danger: 'x', warn: 'alert'`, which made
229
+ // a danger toast render the same bare `x` twice — once as the status, meaning
230
+ // "this failed", once as the close button, meaning "make this go away". Status
231
+ // took the circle glyphs the kit already shipped and never used, and the bare
232
+ // `x` now belongs to the close button alone.
233
+ const TOAST_ICON = {
234
+ success: 'circleCheck', danger: 'circleX', warn: 'circleAlert', info: 'info', neutral: 'bolt',
235
+ };
216
236
  // A toast carries a status (colour) and a style (surface). Everything visual is
217
237
  // token-driven: the status modifier sets --toast-accent/-glow/-on, the style
218
238
  // modifier consumes them. `action` adds a trailing button ("Undo"/"Retry"),
@@ -29,7 +29,7 @@
29
29
  /* ----------------------------------------------------------------------------
30
30
  * Toast — a floating, dismissible notification.
31
31
  * A status (colour) × style (surface) matrix, entirely token-driven:
32
- * status success · danger · warn · info · neutral → sets --toast-accent/-glow/-on/-solid
32
+ * status success · danger · warn · info · neutral → sets --toast-accent/-action-ink/-glow/-on/-solid
33
33
  * style soft (tinted) · solid (filled) · outline (bordered)
34
34
  * -------------------------------------------------------------------------- */
35
35
  .ui-toast {
@@ -51,12 +51,21 @@
51
51
  --toast-accent is the status as a line or a small mark, --toast-glow its wash,
52
52
  --toast-on the glyph ink on the accent circle. --toast-solid is separate on
53
53
  purpose: a full fill is a different job from a 22px circle, and it is chosen
54
- as a PAIR with --signal-solid-ink rather than against a global. See #149. */
55
- .ui-toast--success { --toast-accent: var(--green); --toast-glow: var(--glow-green); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-success); }
56
- .ui-toast--danger { --toast-accent: var(--pink); --toast-glow: var(--glow-pink); --toast-on: var(--danger-contrast); --toast-solid: var(--signal-solid-danger); }
57
- .ui-toast--warn { --toast-accent: var(--amber); --toast-glow: color-mix(in srgb, var(--amber) 14%, transparent); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-warn); }
58
- .ui-toast--info { --toast-accent: var(--cyan); --toast-glow: var(--glow-cyan); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-info); }
59
- .ui-toast--neutral { --toast-accent: var(--muted); --toast-glow: var(--surface-2); --toast-on: var(--strong); --toast-solid: var(--signal-solid-neutral); }
54
+ as a PAIR with --signal-solid-ink rather than against a global. See #149.
55
+
56
+ --toast-action-ink is separate from --toast-accent for the same reason the
57
+ fill is: the accent is a graphic colour, sized for a 3px rule and a 22px
58
+ circle, and in the light theme it is not a colour text can be set in. The
59
+ trailing action is the one part of a toast that is BOTH the status colour and
60
+ a piece of text, so it takes the chip inks, which are the text-grade version
61
+ of the same five statuses. In the dark theme the two are the same value by
62
+ construction — --chip-success-ink IS --green there — so this splits the light
63
+ theme apart and leaves the dark one exactly as it was. */
64
+ .ui-toast--success { --toast-accent: var(--green); --toast-action-ink: var(--chip-success-ink); --toast-glow: var(--glow-green); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-success); }
65
+ .ui-toast--danger { --toast-accent: var(--pink); --toast-action-ink: var(--chip-danger-ink); --toast-glow: var(--glow-pink); --toast-on: var(--danger-contrast); --toast-solid: var(--signal-solid-danger); }
66
+ .ui-toast--warn { --toast-accent: var(--amber); --toast-action-ink: var(--chip-warn-ink); --toast-glow: color-mix(in srgb, var(--amber) 14%, transparent); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-warn); }
67
+ .ui-toast--info { --toast-accent: var(--cyan); --toast-action-ink: var(--chip-info-ink); --toast-glow: var(--glow-cyan); --toast-on: var(--signal-contrast); --toast-solid: var(--signal-solid-info); }
68
+ .ui-toast--neutral { --toast-accent: var(--muted); --toast-action-ink: var(--muted); --toast-glow: var(--surface-2); --toast-on: var(--strong); --toast-solid: var(--signal-solid-neutral); }
60
69
 
61
70
  /* left status marker (soft + outline; solid is already a full fill) */
62
71
  .ui-toast--soft::before,
@@ -71,9 +80,16 @@
71
80
  .ui-toast__title { font-weight: var(--weight-medium); color: var(--strong); }
72
81
  .ui-toast__text { color: var(--dim); }
73
82
 
74
- /* trailing action ("Undo" / "Retry") */
75
- .ui-toast__action { flex: none; align-self: center; background: none; border: 0; padding: 4px 9px; border-radius: var(--radius-xs); font: inherit; font-weight: var(--weight-medium); color: var(--toast-accent); cursor: pointer; }
76
- .ui-toast__action:hover { background: color-mix(in srgb, var(--toast-accent) 14%, transparent); }
83
+ /* trailing action ("Undo" / "Retry") — status-coloured TEXT, so it takes the
84
+ text-grade ink rather than the accent.
85
+ The hover wash is the page background rather than the accent again: a second
86
+ accent tint on an already accent-tinted ground moved the ground TOWARDS the
87
+ ink and took every soft pair below the floor. --bg is white in the light theme
88
+ and near-black in the dark one, so one expression lightens the ground where
89
+ the ink is dark and darkens it where the ink is bright — the hovered pair is
90
+ at or above the resting pair in both. */
91
+ .ui-toast__action { flex: none; align-self: center; background: none; border: 0; padding: 4px 9px; border-radius: var(--radius-xs); font: inherit; font-weight: var(--weight-medium); color: var(--toast-action-ink); cursor: pointer; }
92
+ .ui-toast__action:hover { background: color-mix(in srgb, var(--bg) 55%, transparent); }
77
93
 
78
94
  .ui-toast__close { flex: none; align-self: flex-start; background: none; border: 0; color: var(--muted); cursor: pointer; padding: 2px; display: grid; place-items: center; }
79
95
  .ui-toast__close svg { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 1.8; }
@@ -108,7 +124,10 @@
108
124
  .ui-toast--solid .ui-toast__text { color: color-mix(in srgb, var(--toast-ink) 90%, transparent); }
109
125
  .ui-toast--solid .ui-toast__icon { background: color-mix(in srgb, var(--toast-ink) 22%, transparent); color: var(--toast-ink); }
110
126
  .ui-toast--solid .ui-toast__action { color: var(--toast-ink); }
111
- .ui-toast--solid .ui-toast__action:hover { background: color-mix(in srgb, var(--toast-ink) 18%, transparent); }
127
+ /* Hover darkens the FILL rather than lightening the ink. Washing the ink over
128
+ itself was the one surface #149 did not reach, and it worked against the pair
129
+ it was drawn from: it pulled the fill towards the ink it has to clear. */
130
+ .ui-toast--solid .ui-toast__action:hover { background: color-mix(in srgb, var(--signal-contrast) 14%, transparent); }
112
131
  .ui-toast--solid .ui-toast__close { color: color-mix(in srgb, var(--toast-ink) 68%, transparent); }
113
132
  .ui-toast--solid .ui-toast__close:hover { color: var(--toast-ink); }
114
133
  .ui-toast--solid .ui-toast__timer { background: color-mix(in srgb, var(--toast-ink) 55%, transparent); }