@felloinnovations/brand-kit 0.5.1 → 0.5.3
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/AGENTS.md +29 -9
- package/CHANGELOG.md +86 -0
- package/README.md +10 -4
- package/design-system/01-how-claude-decides.md +5 -3
- package/design-system/03-colour.md +19 -1
- package/design-system/05-space-and-layout.md +2 -1
- package/design-system/08-choosing-components.md +20 -4
- package/design-system/09-charts.md +6 -3
- package/design-system/11-page-recipes.md +40 -1
- package/design-system/13-motion.md +1 -1
- package/design-system/15-ai-experiences.md +11 -0
- package/design-system/17-decisions.md +41 -0
- package/design-system/components/AppShell/README.md +89 -21
- package/design-system/components/AppShell/preview.html +148 -3
- package/design-system/components/AppShell/style.css +47 -0
- package/design-system/components/Avatar/README.md +6 -3
- package/design-system/components/Avatar/preview.html +2 -1
- package/design-system/components/Avatar/style.css +3 -0
- package/design-system/components/Banner/README.md +9 -5
- package/design-system/components/Banner/preview.html +9 -0
- package/design-system/components/Banner/style.css +3 -0
- package/design-system/components/Button/README.md +29 -2
- package/design-system/components/Button/preview.html +33 -1
- package/design-system/components/Button/style.css +21 -0
- package/design-system/components/Calendar/README.md +148 -0
- package/design-system/components/Calendar/preview.html +449 -0
- package/design-system/components/Calendar/style.css +61 -0
- package/design-system/components/Card/README.md +13 -0
- package/design-system/components/Card/preview.html +23 -0
- package/design-system/components/Card/style.css +19 -0
- package/design-system/components/ChatCard/README.md +74 -3
- package/design-system/components/ChatCard/preview.html +126 -25
- package/design-system/components/ChatCard/style.css +6 -0
- package/design-system/components/Chip/README.md +16 -7
- package/design-system/components/Chip/preview.html +25 -1
- package/design-system/components/Chip/style.css +5 -0
- package/design-system/components/Combobox/README.md +10 -4
- package/design-system/components/Combobox/preview.html +19 -4
- package/design-system/components/Combobox/style.css +7 -1
- package/design-system/components/CommentThread/README.md +138 -0
- package/design-system/components/CommentThread/preview.html +134 -0
- package/design-system/components/CommentThread/style.css +41 -0
- package/design-system/components/DateInput/README.md +38 -4
- package/design-system/components/DateInput/preview.html +50 -2
- package/design-system/components/DateInput/style.css +6 -0
- package/design-system/components/Dialog/README.md +26 -3
- package/design-system/components/Dialog/preview.html +80 -0
- package/design-system/components/Dialog/style.css +28 -3
- package/design-system/components/EmptyState/README.md +1 -1
- package/design-system/components/Fold/README.md +21 -0
- package/design-system/components/Fold/preview.html +44 -1
- package/design-system/components/Fold/style.css +30 -0
- package/design-system/components/InlineEdit/README.md +116 -0
- package/design-system/components/InlineEdit/preview.html +135 -0
- package/design-system/components/InlineEdit/style.css +34 -0
- package/design-system/components/Input/README.md +20 -1
- package/design-system/components/Input/preview.html +24 -0
- package/design-system/components/Input/style.css +13 -1
- package/design-system/components/KanbanBoard/README.md +68 -4
- package/design-system/components/KanbanBoard/preview.html +129 -0
- package/design-system/components/KanbanBoard/style.css +31 -0
- package/design-system/components/PlanTimeline/README.md +144 -0
- package/design-system/components/PlanTimeline/preview.html +109 -0
- package/design-system/components/PlanTimeline/style.css +64 -0
- package/design-system/components/README.md +11 -6
- package/design-system/components/Skeleton/README.md +66 -2
- package/design-system/components/Skeleton/preview.html +83 -1
- package/design-system/components/Starter/README.md +1 -1
- package/design-system/components/Starter/preview.html +1 -1
- package/design-system/components/Stepper/README.md +81 -5
- package/design-system/components/Stepper/preview.html +64 -1
- package/design-system/components/Stepper/style.css +29 -0
- package/design-system/components/StreakCalendar/README.md +1 -0
- package/design-system/components/Table/README.md +7 -3
- package/design-system/components/Table/preview.html +15 -0
- package/design-system/components/Table/style.css +2 -0
- package/design-system/components/TextDiff/README.md +96 -0
- package/design-system/components/TextDiff/preview.html +42 -0
- package/design-system/components/TextDiff/style.css +21 -0
- package/design-system/components/Timeline/README.md +3 -1
- package/design-system/components/TrendLine/README.md +38 -7
- package/design-system/components/TrendLine/preview.html +140 -3
- package/design-system/components/TrendLine/style.css +12 -2
- package/design-system/components/bundle.css +483 -7
- package/design-system/tokens.json +15 -1
- package/dist/fello-core.css +2 -2
- package/dist/fello.css +2 -2
- package/dist/fello.scoped.css +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/scoped.js +2 -2
- package/dist/tokens.d.ts +8 -0
- package/dist/tokens.js +8 -0
- package/package.json +7 -2
- package/scripts/check-drift.mjs +325 -0
- package/scripts/token-model.mjs +2 -1
- package/tokens/_dark.css +2 -0
- package/tokens/_product.css +2 -0
- package/tokens/palettes.ts +1 -1
- package/tokens/shadcn-compat.css +1 -1
- package/tokens/shadcn-compat.hsl.css +1 -1
- package/tokens/tokens.css +14 -1
- package/tokens/tokens.md +53 -46
- package/tokens/tokens.root.css +14 -1
- package/tokens/tokens.ts +9 -1
|
@@ -185,6 +185,86 @@
|
|
|
185
185
|
</dialog>
|
|
186
186
|
</div>
|
|
187
187
|
|
|
188
|
+
<div class="ws-stack" style="width: 560px;" data-live>
|
|
189
|
+
<span class="ws-muted">Live, mounted by the app (ws-dialog--mounted, as a Radix Dialog renders it): the scrim and the pane are added on open and play the same moves; on close they take data-leaving (Radix: data-state="closed") and go once their animations end</span>
|
|
190
|
+
<div class="ws-cluster">
|
|
191
|
+
<button type="button" class="ws-btn ws-btn--secondary" data-mount="mnt-dialog">Mount the dialog</button>
|
|
192
|
+
<button type="button" class="ws-btn ws-btn--secondary" data-mount="mnt-drawer">Mount the drawer</button>
|
|
193
|
+
</div>
|
|
194
|
+
<template id="mnt-dialog">
|
|
195
|
+
<div class="ws-dialog__scrim" data-unmount></div>
|
|
196
|
+
<div class="ws-dialog ws-dialog--mounted" role="dialog" aria-modal="true" aria-labelledby="mnt-dialog-title" tabindex="-1" style="max-width: 420px;">
|
|
197
|
+
<header class="ws-head">
|
|
198
|
+
<div>
|
|
199
|
+
<h2 id="mnt-dialog-title" class="ws-title">Assign this call</h2>
|
|
200
|
+
<p class="ws-body">Choose who calls them next.</p>
|
|
201
|
+
</div>
|
|
202
|
+
<button type="button" class="ws-icon-btn ws-dialog__close" aria-label="Close" data-unmount>
|
|
203
|
+
<svg viewBox="0 0 16 16" fill="none"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
204
|
+
</button>
|
|
205
|
+
</header>
|
|
206
|
+
<div class="ws-dialog__content">
|
|
207
|
+
<label class="ws-field">
|
|
208
|
+
<span class="ws-label">Team member</span>
|
|
209
|
+
<input class="ws-input" value="Priya Nair" readonly>
|
|
210
|
+
</label>
|
|
211
|
+
</div>
|
|
212
|
+
<footer class="ws-band">
|
|
213
|
+
<div class="ws-dialog__acts">
|
|
214
|
+
<button type="button" class="ws-btn ws-btn--secondary ws-btn--lg" data-unmount>Cancel</button>
|
|
215
|
+
<button type="button" class="ws-btn ws-btn--primary ws-btn--lg" data-unmount>Assign</button>
|
|
216
|
+
</div>
|
|
217
|
+
</footer>
|
|
218
|
+
</div>
|
|
219
|
+
</template>
|
|
220
|
+
<template id="mnt-drawer">
|
|
221
|
+
<div class="ws-dialog__scrim" data-unmount></div>
|
|
222
|
+
<div class="ws-dialog ws-dialog--drawer ws-dialog--mounted" role="dialog" aria-modal="true" aria-labelledby="mnt-drawer-title" tabindex="-1">
|
|
223
|
+
<header class="ws-head">
|
|
224
|
+
<div>
|
|
225
|
+
<h2 id="mnt-drawer-title" class="ws-title">Priya Nair</h2>
|
|
226
|
+
<p class="ws-body">A complicated sale, cold call.</p>
|
|
227
|
+
</div>
|
|
228
|
+
<button type="button" class="ws-icon-btn ws-dialog__close" aria-label="Close" data-unmount>
|
|
229
|
+
<svg viewBox="0 0 16 16" fill="none"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
230
|
+
</button>
|
|
231
|
+
</header>
|
|
232
|
+
<div class="ws-dialog__content">
|
|
233
|
+
<p class="ws-body">Twelve years in the neighbourhood, referred by two agents on the team.</p>
|
|
234
|
+
</div>
|
|
235
|
+
<footer class="ws-band">
|
|
236
|
+
<div class="ws-dialog__acts">
|
|
237
|
+
<button type="button" class="ws-btn ws-btn--primary ws-btn--lg" data-unmount>Start the call</button>
|
|
238
|
+
</div>
|
|
239
|
+
</footer>
|
|
240
|
+
</div>
|
|
241
|
+
</template>
|
|
242
|
+
</div>
|
|
243
|
+
<script>
|
|
244
|
+
(() => {
|
|
245
|
+
// The app's part: mount both, then on close mark them leaving and remove them once their own animations end
|
|
246
|
+
// (none under reduced motion, so they go at once). A library such as Radix does this with data-state="closed".
|
|
247
|
+
let opener = null;
|
|
248
|
+
const close = (pane) => {
|
|
249
|
+
const parts = [pane.previousElementSibling, pane];
|
|
250
|
+
for (const el of parts) el.setAttribute('data-leaving', '');
|
|
251
|
+
Promise.allSettled(parts.flatMap((el) => el.getAnimations().map((a) => a.finished))).then(() => {
|
|
252
|
+
for (const el of parts) el.remove();
|
|
253
|
+
opener?.focus();
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
for (const btn of document.querySelectorAll('[data-mount]')) btn.addEventListener('click', () => {
|
|
257
|
+
opener = btn;
|
|
258
|
+
const frag = document.getElementById(btn.dataset.mount).content.cloneNode(true);
|
|
259
|
+
const pane = frag.querySelector('.ws-dialog');
|
|
260
|
+
frag.querySelectorAll('[data-unmount]').forEach((el) => el.addEventListener('click', () => close(pane)));
|
|
261
|
+
pane.addEventListener('keydown', (e) => { if (e.key === 'Escape') close(pane); });
|
|
262
|
+
document.body.append(frag);
|
|
263
|
+
pane.focus();
|
|
264
|
+
});
|
|
265
|
+
})();
|
|
266
|
+
</script>
|
|
267
|
+
|
|
188
268
|
</div>
|
|
189
269
|
</body>
|
|
190
270
|
</html>
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
.ws-dialog { --ws-dialog-inset: var(--ws-band-inset); display: flex; flex-direction: column; width: calc(100% - var(--space-xl)); max-width: 42rem; /* raw: Ignite's Dialog cap (max-w-2xl); no width token */ max-height: calc(100dvh - var(--space-xl)); padding: 0; overflow: visible; border: var(--ws-rim) solid var(--ws-tray); border-radius: var(--ws-radius-drawer); background: var(--ws-raised); color: var(--color-text-body); box-shadow: var(--ws-shadow-float); }
|
|
9
9
|
dialog.ws-dialog:not([open]) { display: none; }
|
|
10
10
|
.ws-dialog::backdrop { background: var(--ws-scrim); }
|
|
11
|
+
/* Mounted by the app (--mounted: a library such as Radix renders the pane as a div in a portal, for its focus trap or a
|
|
12
|
+
test runner with no showModal()). The scrim is its own element before the pane (__scrim, Radix's Overlay), the pane
|
|
13
|
+
fixed over the window and centred by its auto margins (a drawer keeps its own), both on z-modal. No translate of the
|
|
14
|
+
app's own: the motion moves the pane. Before the drawer's rules, so its height and margins win. */
|
|
15
|
+
.ws-dialog__scrim { position: fixed; inset: 0; z-index: var(--z-modal); background: var(--ws-scrim); }
|
|
16
|
+
.ws-dialog--mounted { position: fixed; inset: 0; z-index: var(--z-modal); height: fit-content; margin: auto; }
|
|
11
17
|
/* The compact head (K6): one title, at most one line under it, the close and at most two icon acts. Facts, chips and
|
|
12
18
|
buttons go in the content. It never wraps (a card's head drops its aside under a short title; here the close stays
|
|
13
19
|
top right and the title wraps beside it). The close is Seth's 32 px tray square, centred on the title's first line
|
|
@@ -51,9 +57,20 @@ dialog.ws-dialog:not([open]) { display: none; }
|
|
|
51
57
|
content and the foot rise 6 px out of a 3 px blur over .32 s soft, .08 s after it and 22 ms apart, as North's opened
|
|
52
58
|
tile's details). It leaves faster and shorter: 8 px down to .98 over .12 s. A drawer slides in from its edge on the
|
|
53
59
|
grow curve over .55 s ("Card grows open") and goes back over .4 s ("Card closes back into place"). Native <dialog>
|
|
54
|
-
only: @starting-style for the entrance, display and overlay allow-discrete so the exit plays.
|
|
55
|
-
simply appears and goes. */
|
|
56
|
-
dialog.ws-dialog { --ws-dialog-after: 80ms; /* raw: North's details wait .08 s (open.content delay) */ }
|
|
60
|
+
only: @starting-style for the entrance, display and overlay allow-discrete so the exit plays. A mounted pane
|
|
61
|
+
(--mounted) plays the same as keyframes, below. With reduced motion it simply appears and goes. */
|
|
62
|
+
dialog.ws-dialog, .ws-dialog--mounted { --ws-dialog-after: 80ms; /* raw: North's details wait .08 s (open.content delay) */ }
|
|
63
|
+
/* A mounted pane has no [open] to transition: it plays the same moves as keyframes on mount, and on its way out once the
|
|
64
|
+
app marks it leaving (Radix sets data-state="closed" on the pane and the scrim and waits for their animations to end
|
|
65
|
+
before it unmounts them; anything else sets data-leaving and removes them when theirs end, as Menu and Toast do). */
|
|
66
|
+
.ws-dialog--mounted.ws-dialog--drawer { --ws-dialog-edge: calc(100% + var(--ws-stage-inset)); }
|
|
67
|
+
.ws-dialog--mounted.ws-dialog--drawer-start { --ws-dialog-edge: calc(-100% - var(--ws-stage-inset)); }
|
|
68
|
+
@keyframes ws-dialog-in { from { opacity: 0; transform: translateY(var(--space-sm-plus)) scale(var(--ws-open-scale-pane)); } }
|
|
69
|
+
@keyframes ws-dialog-out { to { opacity: 0; transform: translateY(var(--space-xs)) scale(var(--ws-open-scale-pane)); } }
|
|
70
|
+
@keyframes ws-dialog-edge-in { from { transform: translateX(var(--ws-dialog-edge)); } }
|
|
71
|
+
@keyframes ws-dialog-edge-out { to { transform: translateX(var(--ws-dialog-edge)); } }
|
|
72
|
+
@keyframes ws-dialog-fade-in { from { opacity: 0; } }
|
|
73
|
+
@keyframes ws-dialog-fade-out { to { opacity: 0; } }
|
|
57
74
|
@media (prefers-reduced-motion: no-preference) {
|
|
58
75
|
dialog.ws-dialog, dialog.ws-dialog::backdrop { transition: opacity var(--ws-duration-exit) var(--ws-ease-exit), transform var(--ws-duration-exit) var(--ws-ease-exit), overlay var(--ws-duration-exit) var(--ws-ease-exit) allow-discrete, display var(--ws-duration-exit) var(--ws-ease-exit) allow-discrete; }
|
|
59
76
|
dialog.ws-dialog:not([open]) { opacity: 0; transform: translateY(var(--space-xs)) scale(var(--ws-open-scale-pane)); }
|
|
@@ -63,6 +80,14 @@ dialog.ws-dialog { --ws-dialog-after: 80ms; /* raw: North's details wait .08 s (
|
|
|
63
80
|
dialog.ws-dialog--drawer[open] { transition: opacity var(--ws-duration-grow) var(--ws-ease-grow), transform var(--ws-duration-grow) var(--ws-ease-grow), overlay 0s allow-discrete, display 0s allow-discrete; }
|
|
64
81
|
dialog.ws-dialog--drawer:not([open]) { transform: translateX(calc(100% + var(--ws-stage-inset))); transition-duration: var(--ws-duration-shrink); transition-timing-function: var(--ws-ease-grow); }
|
|
65
82
|
dialog.ws-dialog--drawer-start:not([open]) { transform: translateX(calc(-100% - var(--ws-stage-inset))); }
|
|
83
|
+
.ws-dialog--mounted { animation: ws-dialog-in var(--ws-spring-settle-duration) var(--ws-spring-settle) backwards; }
|
|
84
|
+
.ws-dialog--mounted.ws-dialog--drawer { animation: ws-dialog-edge-in var(--ws-duration-grow) var(--ws-ease-grow) backwards; }
|
|
85
|
+
.ws-dialog--mounted:is([data-leaving], [data-state="closed"]) { animation: ws-dialog-out var(--ws-duration-exit) var(--ws-ease-exit) forwards; pointer-events: none; }
|
|
86
|
+
.ws-dialog--mounted.ws-dialog--drawer:is([data-leaving], [data-state="closed"]) { animation: ws-dialog-edge-out var(--ws-duration-shrink) var(--ws-ease-grow) forwards; }
|
|
87
|
+
.ws-dialog__scrim { animation: ws-dialog-fade-in var(--ws-duration-hover) var(--ws-ease-soft) backwards; }
|
|
88
|
+
.ws-dialog__scrim:is([data-leaving], [data-state="closed"]) { animation: ws-dialog-fade-out var(--ws-duration-exit) var(--ws-ease-exit) forwards; }
|
|
89
|
+
.ws-dialog--mounted:not([data-leaving], [data-state="closed"]) > * { animation: ws-settle var(--ws-duration-settle) var(--ws-ease-soft) var(--ws-dialog-after) backwards; }
|
|
90
|
+
@supports (animation-delay: calc(sibling-index() * 1ms)) { .ws-dialog--mounted:not([data-leaving], [data-state="closed"]) > * { animation-delay: calc(var(--ws-dialog-after) + (sibling-index() - 1) * var(--ws-stagger-detail)); } }
|
|
66
91
|
dialog.ws-dialog[open] > * { animation: ws-settle var(--ws-duration-settle) var(--ws-ease-soft) var(--ws-dialog-after) backwards; }
|
|
67
92
|
@supports (animation-delay: calc(sibling-index() * 1ms)) { dialog.ws-dialog[open] > * { animation-delay: calc(var(--ws-dialog-after) + (sibling-index() - 1) * var(--ws-stagger-detail)); } }
|
|
68
93
|
@starting-style {
|
|
@@ -9,7 +9,7 @@ Nothing here yet: the same plain cream card the place would show, with its mark
|
|
|
9
9
|
## Not for
|
|
10
10
|
|
|
11
11
|
- A card whose data genuinely does not exist and never will for this account: leave the card out of the page entirely, do not draw an empty shell for it (the system's rule, "a card whose data does not exist is left out, never faked").
|
|
12
|
-
- A page still loading its data: use `
|
|
12
|
+
- A page still loading its data: use `Skeleton` (a whole page: its page ghost), not an empty state (an empty state means "there is an answer, and it is nothing", not "the answer has not arrived yet").
|
|
13
13
|
- An error the person must act on: that is a field or page error, not an empty state.
|
|
14
14
|
|
|
15
15
|
## Anatomy
|
|
@@ -31,6 +31,20 @@ The one disclosure: something folded away until wanted, in three looks, built on
|
|
|
31
31
|
and line) taking the room and the chevron at the end. The body sits under the words' column, 0 / 16 / 16 / 20, and
|
|
32
32
|
may hold a cream panel (`ws-fold__panel`: 6 px corners, 24 px inside).
|
|
33
33
|
|
|
34
|
+
- **More in a card fold's head** (card look only):
|
|
35
|
+
- **Acts** (`ws-fold` on the card, `ws-fold__act` after the `<details>`): one or two buttons beside the summary
|
|
36
|
+
("Publish", "Add") that never toggle it. A button may not sit inside a `<summary>` (a control inside a control), so
|
|
37
|
+
the card is a grid and the details a subgrid of it: the summary takes the first column, the acts the second on the
|
|
38
|
+
summary's row, centred on its first 44 (the title's band), and the body runs under both. A long title wraps beside
|
|
39
|
+
the acts, never under them. They show open or folded. At most one primary act on the page; here usually a secondary
|
|
40
|
+
or a text button.
|
|
41
|
+
- **An end status**: the summary's note after the title (`ws-muted`, "Autosaved", "3 open"), or a `Badge` for a count.
|
|
42
|
+
It sits at the far end, before the acts, and drops under a long title when there is no room.
|
|
43
|
+
- **Folded words** (`ws-fold__peek`, the summary's last part): a line under the title, past the chevron, that shows
|
|
44
|
+
only while the fold is shut (a strip of variant chips). It is inside the summary, so words and marks only, never an act.
|
|
45
|
+
- **Tone** (`ws-fold--needs`, `--positive`, `--working` on the card): Banner's three tints for a fold that is a notice
|
|
46
|
+
("3 checks failed"). The ground takes the tint, the title and the chevron the tone's words, a hover the navy wash.
|
|
47
|
+
|
|
34
48
|
## Variants and sizes
|
|
35
49
|
|
|
36
50
|
| Look | Chevron | Ground | Use |
|
|
@@ -38,6 +52,8 @@ The one disclosure: something folded away until wanted, in three looks, built on
|
|
|
38
52
|
| `card` (default) | Before the title, right, turns down | A full `Card` | A self-contained block of extra detail |
|
|
39
53
|
| `quiet` | After the words, right, turns down | None | A short reasoning trail beside other content |
|
|
40
54
|
| `row` | At the end, down, turns up | A row of a rim list; open, the sunk cream | A chapter or step inside a list |
|
|
55
|
+
| `card` + `ws-fold` + `ws-fold__act` | As `card` | A full `Card` | A block whose head carries an act that must not toggle it |
|
|
56
|
+
| `card` + `ws-fold--needs` / `--positive` / `--working` | As `card`, in the tone's words | The tone's tint | A fold that is a notice |
|
|
41
57
|
|
|
42
58
|
## States
|
|
43
59
|
|
|
@@ -51,6 +67,8 @@ The one disclosure: something folded away until wanted, in three looks, built on
|
|
|
51
67
|
- **Focus**: the 2 px navy outline; on the card and row summaries it is drawn inside, so the rim and the dividers stay
|
|
52
68
|
clear. Native `<details>` keyboard behaviour is kept.
|
|
53
69
|
- **Controlled**: the page may set `open` itself (a link that opens one chapter).
|
|
70
|
+
- **With acts**: the acts' room tints with the summary under the pointer and ends at the card's corners; pressing an act
|
|
71
|
+
never opens or closes the fold. A peek hides as the fold opens and comes back as it shuts.
|
|
54
72
|
- **Long label**: the title wraps beside its chevron; a note drops to the far end and under the title when there is no
|
|
55
73
|
room. Nothing is cut off.
|
|
56
74
|
- **Touch**: card and row summaries are at least 40 px (44 and 48), the quiet look 24.
|
|
@@ -85,6 +103,9 @@ The one disclosure: something folded away until wanted, in three looks, built on
|
|
|
85
103
|
| `open` | boolean | How it starts. |
|
|
86
104
|
| `onOpenChange` | `(open: boolean) => void` | The page keeps the open state and hears every change. |
|
|
87
105
|
| `look` | `"card"` \| `"quiet"` \| `"row"` | Which of the three looks to draw. Default `card`. |
|
|
106
|
+
| `acts` | `ReactNode` | Buttons beside the summary that never toggle it (`card` look; `ws-fold__act`). |
|
|
107
|
+
| `peek` | `ReactNode` | Words or marks shown under the title only while folded (`ws-fold__peek`). |
|
|
108
|
+
| `tone` | `"needs"` \| `"positive"` \| `"working"` | A fold that is a notice: the tone's tint and words (`card` look). |
|
|
88
109
|
| `label` | string | The summary's accessible name when its words are a shorthand ("3 steps, 4 s"). |
|
|
89
110
|
| `className` | string | Layout only. |
|
|
90
111
|
| `children` | `ReactNode` | The folded body. |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Cards" height=
|
|
1
|
+
<!-- @dsCard group="Cards" height=520 -->
|
|
2
2
|
<!doctype html>
|
|
3
3
|
<html>
|
|
4
4
|
<head>
|
|
@@ -49,6 +49,49 @@
|
|
|
49
49
|
</div>
|
|
50
50
|
</div>
|
|
51
51
|
|
|
52
|
+
<div class="ws-stack ws-stack--loose" style="width: 400px;">
|
|
53
|
+
<div class="ws-stack">
|
|
54
|
+
<span class="ws-muted">Card look with an act beside the summary (it never toggles the fold), a status at the end, and the variants peeking while it is folded</span>
|
|
55
|
+
<div class="ws-card ws-fold">
|
|
56
|
+
<details>
|
|
57
|
+
<summary class="ws-fold__summary ws-fold__summary--card">
|
|
58
|
+
<svg class="ws-fold__chevron ws-fold__chevron--right" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M6.6 5.2L9.4 8l-2.8 2.8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
59
|
+
<span class="ws-fold__title">Ad copy for the open house weekend</span>
|
|
60
|
+
<span class="ws-muted">Autosaved</span>
|
|
61
|
+
<span class="ws-fold__peek">
|
|
62
|
+
<span class="ws-chip">Version 1</span>
|
|
63
|
+
<span class="ws-chip">Version 2</span>
|
|
64
|
+
<span class="ws-chip">Version 3</span>
|
|
65
|
+
</span>
|
|
66
|
+
</summary>
|
|
67
|
+
<div class="ws-fold__body--card">
|
|
68
|
+
<p class="ws-body">Three versions, each under 125 characters, the second leading with the price.</p>
|
|
69
|
+
</div>
|
|
70
|
+
</details>
|
|
71
|
+
<div class="ws-fold__act">
|
|
72
|
+
<button type="button" class="ws-btn ws-btn--secondary ws-btn--sm">Publish</button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="ws-stack">
|
|
77
|
+
<span class="ws-muted">A fold that is a notice: the needs tone on the whole card, its title and chevron in the tone's words</span>
|
|
78
|
+
<div class="ws-card ws-fold ws-fold--needs">
|
|
79
|
+
<details>
|
|
80
|
+
<summary class="ws-fold__summary ws-fold__summary--card">
|
|
81
|
+
<svg class="ws-fold__chevron ws-fold__chevron--right" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M6.6 5.2L9.4 8l-2.8 2.8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
82
|
+
<span class="ws-fold__title">3 checks failed</span>
|
|
83
|
+
</summary>
|
|
84
|
+
<div class="ws-fold__body--card">
|
|
85
|
+
<p class="ws-body">The headline runs past 40 characters, the price is missing and the image is under 1080 wide.</p>
|
|
86
|
+
</div>
|
|
87
|
+
</details>
|
|
88
|
+
<div class="ws-fold__act">
|
|
89
|
+
<button type="button" class="ws-btn ws-btn--text ws-btn--sm">Fix them</button>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
52
95
|
<div class="ws-stack" style="width: 556px;">
|
|
53
96
|
<span class="ws-muted">Row look, in a rim list: the open row and its body sunk, a cream panel inside</span>
|
|
54
97
|
<ul class="ws-fold__list">
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
/* Card look: the summary is 44 tall, 4 / 16 (16 all round made it 48 and more); the body 4 / 16 / 16 under it. */
|
|
5
5
|
.ws-fold__summary--card { min-height: calc(var(--space-2xl) + var(--space-xxs)); flex-wrap: wrap; gap: var(--space-xs); border-radius: var(--ws-radius-card-inner); padding: var(--space-xxs) var(--ws-card-padding-lg); }
|
|
6
6
|
.ws-fold__summary--card > .ws-fold__title { flex: 1 1 0; min-width: min(100%, var(--space-8xl)); }
|
|
7
|
+
/* The note or end status keeps to the far end, on the title's line or, with no room, on its own line under it. */
|
|
8
|
+
.ws-fold__summary--card > :not(.ws-fold__chevron, .ws-fold__title, .ws-fold__peek) { margin-left: auto; }
|
|
7
9
|
details[open] > .ws-fold__summary--card { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
|
8
10
|
.ws-fold__summary--card:focus-visible { outline-offset: calc(var(--ws-focus-width) * -1); }
|
|
9
11
|
.ws-fold__body--card { padding: var(--space-xxs) var(--ws-card-padding-lg) var(--ws-card-padding-lg); }
|
|
@@ -46,3 +48,31 @@ details[open] > summary > .ws-fold__chevron--down { transform: rotate(180deg); }
|
|
|
46
48
|
@media (prefers-reduced-motion: no-preference) {
|
|
47
49
|
.ws-fold__chevron { transition: transform var(--ws-spring-fold-duration) var(--ws-spring-fold); }
|
|
48
50
|
}
|
|
51
|
+
/* A card fold with more in its head (Madan, Marketing OS): acts that must not toggle it, words that show only while it
|
|
52
|
+
is folded, and a tone for the whole block.
|
|
53
|
+
Acts (ws-fold, the card around the details, and ws-fold__act after it): a button may not sit inside a summary (one
|
|
54
|
+
control inside another), so the acts sit beside it in the card's own grid. The details is a subgrid of the card's two
|
|
55
|
+
columns and rows: the summary takes the first column, the acts the second on the summary's row (filling it, so
|
|
56
|
+
its tint is whole; the acts centred on the summary's first 44, the title's band, not on a peek under it), the body
|
|
57
|
+
both columns under them. A long title wraps beside the acts instead of running under them. The acts' room tints with the summary under the pointer. */
|
|
58
|
+
.ws-fold { display: grid; grid-template-columns: minmax(0, 1fr) auto; }
|
|
59
|
+
.ws-fold > details { display: grid; grid-template-columns: subgrid; grid-template-rows: subgrid; grid-column: 1 / -1; grid-row: 1 / span 2; border-radius: inherit; }
|
|
60
|
+
.ws-fold > details > summary { grid-column: 1; border-top-right-radius: 0; border-bottom-right-radius: 0; }
|
|
61
|
+
.ws-fold > details > :not(summary), .ws-fold > details::details-content { grid-column: 1 / -1; }
|
|
62
|
+
.ws-fold__act { display: grid; grid-auto-flow: column; grid-template-rows: calc(var(--space-2xl) + var(--space-xxs)); grid-column: 2; grid-row: 1; align-content: start; align-items: center; gap: var(--space-xs); padding-right: var(--ws-card-padding-lg); border-top-right-radius: var(--ws-radius-card-inner); transition: background-color var(--ws-duration-hover) var(--ws-ease-soft); }
|
|
63
|
+
.ws-fold:has(> details:not([open])) > .ws-fold__act { border-bottom-right-radius: var(--ws-radius-card-inner); }
|
|
64
|
+
@media (hover: hover) { .ws-fold:has(> details > summary:hover) > .ws-fold__act { background-color: var(--ws-fold-hover, var(--ws-surface-sunk)); } }
|
|
65
|
+
/* Folded words (ws-fold__peek, the summary's last part): a line under the title, past the chevron, that shows only while
|
|
66
|
+
the fold is shut (a strip of variant chips). It is inside the summary, so it holds words and marks, never an act. */
|
|
67
|
+
.ws-fold__peek { display: flex; flex-wrap: wrap; flex-basis: 100%; gap: var(--space-xxs); padding: 0 0 var(--space-sm) calc(var(--ws-icon) + var(--space-xs)); }
|
|
68
|
+
details[open] > summary > .ws-fold__peek { display: none; }
|
|
69
|
+
/* Tone (ws-fold--needs, --positive, --working on the card): Banner's three tints and words, for a fold that is a notice
|
|
70
|
+
(three checks failed, the import finished). The ground takes the tint, the title and the chevron the tone's words; a
|
|
71
|
+
hover is the navy wash over the tint, not the sunk cream. */
|
|
72
|
+
.ws-fold--needs { --ws-fold-tint: var(--ws-tint-needs); --ws-fold-word: var(--ws-word-needs); }
|
|
73
|
+
.ws-fold--positive { --ws-fold-tint: var(--ws-tint-live); --ws-fold-word: var(--ws-word-positive); }
|
|
74
|
+
.ws-fold--working { --ws-fold-tint: var(--ws-card-tint-blue); --ws-fold-word: var(--color-text-interactive); }
|
|
75
|
+
:is(.ws-fold--needs, .ws-fold--positive, .ws-fold--working) { --ws-fold-hover: var(--ws-hover); background-color: var(--ws-fold-tint); }
|
|
76
|
+
:is(.ws-fold--needs, .ws-fold--positive, .ws-fold--working) .ws-fold__summary :is(.ws-fold__title, .ws-fold__chevron) { color: var(--ws-fold-word); }
|
|
77
|
+
@media (hover: hover) { :is(.ws-fold--needs, .ws-fold--positive, .ws-fold--working) .ws-fold__summary--card:hover { background-color: var(--ws-hover); } }
|
|
78
|
+
:is(.ws-fold--needs, .ws-fold--positive, .ws-fold--working) .ws-fold__summary--card:active { background-color: var(--ws-hover-strong); }
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Words that become a field in place: a value read and edited where it sits (a task's title, a brief's section, a
|
|
2
|
+
milestone's name), the same box and the same type at rest and editing, so nothing jumps.
|
|
3
|
+
|
|
4
|
+
## Use it when
|
|
5
|
+
|
|
6
|
+
- One value is edited where it is read, and editing it is the exception, not the job of the screen: a title in a
|
|
7
|
+
drawer's head, a fact in a brief, a row's name.
|
|
8
|
+
- The person changes one thing at a time and each change saves on its own (Patterns, "One small change").
|
|
9
|
+
|
|
10
|
+
## Not for
|
|
11
|
+
|
|
12
|
+
- A form, or several related values saved together: `Input` and `TextArea` with labels, and a Save in the band.
|
|
13
|
+
- A value with a closed set of choices (a stage, an owner): a `Select` or a `Combobox`, opened from the value.
|
|
14
|
+
- Words the person cannot change: plain text, never a button that does nothing.
|
|
15
|
+
- A setting that applies at once: `Switch`.
|
|
16
|
+
|
|
17
|
+
## Anatomy
|
|
18
|
+
|
|
19
|
+
- The wrapper (`ws-inline-edit`) wears the value's own type: `ws-title`, `ws-section` or `ws-body`. Everything inside
|
|
20
|
+
inherits it, so the field is the words' size, weight and colour.
|
|
21
|
+
- The value (`__value`): at rest a `<button>` holding the words; editing, an `<input>` (one line) or a `<textarea>`
|
|
22
|
+
(several) in its place. Both are the same box: 4 / 8 inside, 4 px corners (`--ws-radius-control`), pulled back 8 at
|
|
23
|
+
the sides so the words stay on the content edge of everything around them. On touch the box is 40 tall at least, at
|
|
24
|
+
rest and editing alike.
|
|
25
|
+
- At rest: no ground, no edge; North's navy wash (`--ws-hover`) under the pointer, a step stronger pressed. The words
|
|
26
|
+
in a hidden prefix name the act for a screen reader (`<span class="ws-sr">Edit the title: </span>`).
|
|
27
|
+
- The pencil (`__mark`, a `ws-icon` at the words' size, 16 at most, muted), 4 after the last word: it shows under the
|
|
28
|
+
pointer and on focus, and always on touch, where there is no hover. A hint only: the whole box is the target.
|
|
29
|
+
- Editing: the field's white ground (`--ws-field`) and its fine edge (`--ws-shadow-hairline-in`), the focus ring drawn
|
|
30
|
+
inside the box (so it never crosses a label 4 above), the words in their own colour. A textarea grows with its words
|
|
31
|
+
(`field-sizing: content`).
|
|
32
|
+
- The save's word under it: the house field message (`ws-field-msg`, `role="status"`): "Saving", then "Saved" for a
|
|
33
|
+
moment; or the error message (`ws-field-msg--error`) with the warning mark.
|
|
34
|
+
|
|
35
|
+
## Variants and sizes
|
|
36
|
+
|
|
37
|
+
| Wrapper | Where | Notes |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `ws-inline-edit ws-title` | a drawer's or a page's own title | 22 / 28 semibold navy, one line (`<input>`) |
|
|
40
|
+
| `ws-inline-edit ws-section` | a card's or a task's title | 15 / 22 semibold navy |
|
|
41
|
+
| `ws-inline-edit ws-body` | a fact, a section of a brief, a name in a row | 14 / 20; several lines take a `<textarea>` |
|
|
42
|
+
|
|
43
|
+
## States
|
|
44
|
+
|
|
45
|
+
- **Rest**: the words, no ground; the pencil hidden (shown on touch).
|
|
46
|
+
- **Hover** (a pointer only): the navy wash and the pencil. Never a lift or an edge.
|
|
47
|
+
- **Focus** (Tab onto it): the focus ring inside the box and the pencil.
|
|
48
|
+
- **Editing**: the field, focused, its words selected on open so typing replaces them.
|
|
49
|
+
- **Empty**: the placeholder's words at rest (`data-empty`: "Add an owner"), the same target.
|
|
50
|
+
- **Saving**: the value shows the new words at once; "Saving" under it while the save is on its way. Typing again
|
|
51
|
+
meanwhile keeps the newer words (only the latest save's outcome shows).
|
|
52
|
+
- **Saved**: "Saved" for a moment (2 s), then nothing.
|
|
53
|
+
- **Not saved**: the field stays open with the person's words and the error under it ("Not saved. Try again", or what
|
|
54
|
+
is wrong: "The campaign needs a name"), `aria-invalid` and `aria-describedby` set together. Nothing typed is lost.
|
|
55
|
+
- **Disabled or read-only**: plain words, no button.
|
|
56
|
+
- **Touch**: the box 40 tall at least; the editing words 16 at least (a phone zooms the page into a field under 16).
|
|
57
|
+
- **Dark**: the wash, the field's ground and its ink take their dark values.
|
|
58
|
+
|
|
59
|
+
## Behaviour
|
|
60
|
+
|
|
61
|
+
- Press (click, tap, Enter or Space on the button): the button is replaced by the field with the same words, focused,
|
|
62
|
+
the words selected.
|
|
63
|
+
- **Enter saves** (in a one-line field; in a textarea Enter is a new line and Cmd or Ctrl Enter saves). **Blur saves.**
|
|
64
|
+
**Escape cancels**: the words go back to what was saved and nothing is sent.
|
|
65
|
+
- Either way, the field is replaced by the button again and **focus returns to it**, so the keyboard stays where it was.
|
|
66
|
+
- Saving the same words sends nothing. A required value emptied is refused in place, not sent.
|
|
67
|
+
- A textarea where the browser has no `field-sizing` (Firefox, older Safari): set `rows` to the words' line count on
|
|
68
|
+
open so it does not shrink to two lines.
|
|
69
|
+
- Motion: the wash and the pencil over the hover beat (`--ws-duration-hover`, `--ws-ease-soft`); the swap itself does
|
|
70
|
+
not animate. Still under reduced motion.
|
|
71
|
+
|
|
72
|
+
## Content
|
|
73
|
+
|
|
74
|
+
- The placeholder says what goes there in sentence case ("Add an owner"), never "Click to edit".
|
|
75
|
+
- The hidden prefix names the value ("Edit the title: "), so a screen reader hears what it changes.
|
|
76
|
+
- An error says what to do. No em dashes.
|
|
77
|
+
|
|
78
|
+
## Rules
|
|
79
|
+
|
|
80
|
+
- Same box, same type, at rest and editing: the wrapper carries the type, never the field. A field that changes size
|
|
81
|
+
or weight when it opens is the bug this exists to stop.
|
|
82
|
+
- One value per inline edit, saved on its own. Several values that belong together are a form.
|
|
83
|
+
- Never the only way to reach the value on touch: the pencil is always shown there.
|
|
84
|
+
- Never an underline or a dotted edge at rest to say "editable": the wash and the pencil do it.
|
|
85
|
+
|
|
86
|
+
## Source
|
|
87
|
+
|
|
88
|
+
- **Derived from**: Patterns' "Editing a record" (one small change: inline, saves on blur or Enter, Escape reverts,
|
|
89
|
+
"Saved" for a moment), `Input`'s field ground, edge, focus ring and error, North's wash.
|
|
90
|
+
- **Precedent**: Marketing OS (ContentOS), the task drawer's title and the brief document's fields, about 55 hand-styled
|
|
91
|
+
inputs (issue #25, Madan, 10 Nov 2026).
|
|
92
|
+
|
|
93
|
+
## Markup
|
|
94
|
+
|
|
95
|
+
```html
|
|
96
|
+
<!-- At rest -->
|
|
97
|
+
<div class="ws-inline-edit ws-section">
|
|
98
|
+
<button type="button" class="ws-inline-edit__value"><span class="ws-sr">Edit the title: </span>Write the spring market email<svg class="ws-icon ws-inline-edit__mark" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.5 3.5l2 2L6 12H4v-2l6.5-6.5z"/></svg></button>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<!-- Editing (the button replaced in place) -->
|
|
102
|
+
<div class="ws-inline-edit ws-section">
|
|
103
|
+
<input class="ws-inline-edit__value" type="text" value="Write the spring market email" aria-label="Title">
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<!-- Several lines, saving -->
|
|
107
|
+
<div class="ws-inline-edit ws-body">
|
|
108
|
+
<textarea class="ws-inline-edit__value" aria-label="Goal">Book forty demos from the email and the two webinars in March.</textarea>
|
|
109
|
+
<p class="ws-field-msg" role="status">Saving</p>
|
|
110
|
+
</div>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Styles
|
|
114
|
+
|
|
115
|
+
`design-system/components/InlineEdit/style.css`: `ws-inline-edit`, `__value` (and `[data-empty]`, `--typing`, the
|
|
116
|
+
focus drawn still for a mockup), `__mark`. The message is the house's `ws-field-msg`.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<!-- @dsCard group="Fields" height=640 -->
|
|
2
|
+
<!doctype html>
|
|
3
|
+
<html>
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>InlineEdit</title>
|
|
7
|
+
<link rel="stylesheet" href="../../tokens.css">
|
|
8
|
+
<link rel="stylesheet" href="../bundle.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div class="ws-stage">
|
|
12
|
+
<div class="ws-stack" style="width: 340px;">
|
|
13
|
+
<span class="ws-muted">A task's title at rest: plain words, a wash and the pencil under the pointer</span>
|
|
14
|
+
<div class="ws-card ws-card--pad-md ws-stack">
|
|
15
|
+
<span class="ws-meta">Task</span>
|
|
16
|
+
<div class="ws-inline-edit ws-section">
|
|
17
|
+
<button type="button" class="ws-inline-edit__value"><span class="ws-sr">Edit the title: </span>Write the spring market email<svg class="ws-icon ws-inline-edit__mark" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.5 3.5l2 2L6 12H4v-2l6.5-6.5z"/></svg></button>
|
|
18
|
+
</div>
|
|
19
|
+
<p class="ws-body">Due Friday. Dana owns the draft.</p>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="ws-stack" style="width: 340px;">
|
|
24
|
+
<span class="ws-muted">The same title being edited: the field takes its box and its type, so nothing moves</span>
|
|
25
|
+
<div class="ws-card ws-card--pad-md ws-stack">
|
|
26
|
+
<span class="ws-meta">Task</span>
|
|
27
|
+
<div class="ws-inline-edit ws-section">
|
|
28
|
+
<input class="ws-inline-edit__value ws-inline-edit__value--typing" type="text" value="Write the spring market email" aria-label="Title">
|
|
29
|
+
</div>
|
|
30
|
+
<p class="ws-body">Due Friday. Dana owns the draft.</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="ws-stack" style="width: 340px;">
|
|
35
|
+
<span class="ws-muted">A brief's section: several lines at rest, then a textarea that grows with its words</span>
|
|
36
|
+
<div class="ws-card ws-card--pad-md ws-stack ws-stack--loose">
|
|
37
|
+
<div class="ws-stack ws-stack--tight">
|
|
38
|
+
<span class="ws-meta">Audience</span>
|
|
39
|
+
<div class="ws-inline-edit ws-body">
|
|
40
|
+
<button type="button" class="ws-inline-edit__value"><span class="ws-sr">Edit the audience: </span>Agents in Ohio who closed fewer than five deals last year and still farm a neighbourhood.<svg class="ws-icon ws-inline-edit__mark" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.5 3.5l2 2L6 12H4v-2l6.5-6.5z"/></svg></button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="ws-stack ws-stack--tight">
|
|
44
|
+
<span class="ws-meta">Goal</span>
|
|
45
|
+
<div class="ws-inline-edit ws-body">
|
|
46
|
+
<textarea class="ws-inline-edit__value ws-inline-edit__value--typing" aria-label="Goal" rows="2">Book forty demos from the email and the two webinars in March.</textarea>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="ws-stack" style="width: 340px;">
|
|
53
|
+
<span class="ws-muted">No value yet, then a save on its way, saved, and refused</span>
|
|
54
|
+
<div class="ws-card ws-card--pad-md ws-stack ws-stack--loose">
|
|
55
|
+
<div class="ws-stack ws-stack--tight">
|
|
56
|
+
<span class="ws-meta">Owner</span>
|
|
57
|
+
<div class="ws-inline-edit ws-body">
|
|
58
|
+
<button type="button" class="ws-inline-edit__value" data-empty><span class="ws-sr">Edit the owner: </span>Add an owner<svg class="ws-icon ws-inline-edit__mark" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.5 3.5l2 2L6 12H4v-2l6.5-6.5z"/></svg></button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="ws-stack ws-stack--tight">
|
|
62
|
+
<span class="ws-meta">Channel</span>
|
|
63
|
+
<div class="ws-inline-edit ws-body">
|
|
64
|
+
<button type="button" class="ws-inline-edit__value"><span class="ws-sr">Edit the channel: </span>Email and two webinars<svg class="ws-icon ws-inline-edit__mark" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.5 3.5l2 2L6 12H4v-2l6.5-6.5z"/></svg></button>
|
|
65
|
+
<p class="ws-field-msg" role="status">Saving</p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="ws-stack ws-stack--tight">
|
|
69
|
+
<span class="ws-meta">Budget</span>
|
|
70
|
+
<div class="ws-inline-edit ws-body">
|
|
71
|
+
<button type="button" class="ws-inline-edit__value"><span class="ws-sr">Edit the budget: </span>$4,000<svg class="ws-icon ws-inline-edit__mark" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.5 3.5l2 2L6 12H4v-2l6.5-6.5z"/></svg></button>
|
|
72
|
+
<p class="ws-field-msg" role="status">Saved</p>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="ws-stack ws-stack--tight">
|
|
76
|
+
<span class="ws-meta">Campaign name</span>
|
|
77
|
+
<div class="ws-inline-edit ws-body">
|
|
78
|
+
<input class="ws-inline-edit__value" type="text" value="" aria-label="Campaign name" aria-invalid="true" aria-describedby="ie-name-msg" placeholder="Spring market">
|
|
79
|
+
<p class="ws-field-msg ws-field-msg--error" id="ie-name-msg"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="6"/><path d="M8 5v3.5M8 11h.01"/></svg>The campaign needs a name</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="ws-stack" style="width: 340px;">
|
|
86
|
+
<span class="ws-muted">Live: press the words, type, Enter saves, Escape puts them back; focus returns to the words</span>
|
|
87
|
+
<div class="ws-card ws-card--pad-md ws-stack">
|
|
88
|
+
<span class="ws-meta">Milestone</span>
|
|
89
|
+
<div class="ws-inline-edit ws-section" id="ie-live">
|
|
90
|
+
<button type="button" class="ws-inline-edit__value"><span class="ws-sr">Edit the milestone: </span><span data-words>First draft to Nikhil</span><svg class="ws-icon ws-inline-edit__mark" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.5 3.5l2 2L6 12H4v-2l6.5-6.5z"/></svg></button>
|
|
91
|
+
<p class="ws-field-msg" role="status" data-status></p>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
<script>
|
|
97
|
+
(() => {
|
|
98
|
+
// The behaviour an app wires: the button becomes an input with the same words, Enter or blur saves, Escape cancels,
|
|
99
|
+
// and focus goes back to the button either way.
|
|
100
|
+
const root = document.getElementById('ie-live');
|
|
101
|
+
const button = root.querySelector('button');
|
|
102
|
+
const words = button.querySelector('[data-words]');
|
|
103
|
+
const status = root.querySelector('[data-status]');
|
|
104
|
+
button.addEventListener('click', () => {
|
|
105
|
+
const input = document.createElement('input');
|
|
106
|
+
input.className = 'ws-inline-edit__value';
|
|
107
|
+
input.type = 'text';
|
|
108
|
+
input.value = words.textContent;
|
|
109
|
+
input.setAttribute('aria-label', 'Milestone');
|
|
110
|
+
let done = false;
|
|
111
|
+
const finish = (save) => {
|
|
112
|
+
if (done) return;
|
|
113
|
+
done = true;
|
|
114
|
+
const next = input.value.trim();
|
|
115
|
+
if (save && next && next !== words.textContent) {
|
|
116
|
+
words.textContent = next;
|
|
117
|
+
status.textContent = 'Saved';
|
|
118
|
+
}
|
|
119
|
+
input.replaceWith(button);
|
|
120
|
+
button.focus();
|
|
121
|
+
};
|
|
122
|
+
input.addEventListener('keydown', (e) => {
|
|
123
|
+
if (e.key === 'Enter') { e.preventDefault(); finish(true); }
|
|
124
|
+
if (e.key === 'Escape') { e.preventDefault(); finish(false); }
|
|
125
|
+
});
|
|
126
|
+
input.addEventListener('blur', () => finish(true));
|
|
127
|
+
status.textContent = '';
|
|
128
|
+
button.replaceWith(input);
|
|
129
|
+
input.focus();
|
|
130
|
+
input.select();
|
|
131
|
+
});
|
|
132
|
+
})();
|
|
133
|
+
</script>
|
|
134
|
+
</body>
|
|
135
|
+
</html>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* ---- InlineEdit: words that become a field in place. The rest state is a button holding the words, the editing state
|
|
2
|
+
an input or a textarea; both are __value, so they share one box (4 / 8 inside, pulled back 8 at the sides so the words
|
|
3
|
+
stay on the content edge) and the words' own type (the wrapper wears ws-section, ws-title or ws-body), and nothing jumps when
|
|
4
|
+
one turns into the other. At rest: no ground, a navy wash under the pointer, a pencil mark that shows on hover and
|
|
5
|
+
focus (always on touch, where there is no hover). Editing: the field's white ground, its fine edge and the focus
|
|
6
|
+
ring, the words keeping their own colour and weight. ---- */
|
|
7
|
+
.ws-inline-edit { display: grid; gap: var(--space-3xs); min-width: 0; }
|
|
8
|
+
.ws-inline-edit__value { display: block; box-sizing: border-box; width: calc(100% + var(--space-xs) * 2); min-width: 0; margin: 0 calc(var(--space-xs) * -1); padding: var(--space-xxs) var(--space-xs); border: 0; border-radius: var(--ws-radius-control); background: transparent; color: inherit; font: inherit; letter-spacing: inherit; text-align: start; text-wrap: inherit; overflow-wrap: anywhere; }
|
|
9
|
+
button.ws-inline-edit__value { cursor: text; transition: background-color var(--ws-duration-hover) var(--ws-ease-soft); }
|
|
10
|
+
@media (hover: hover) { button.ws-inline-edit__value:hover { background: var(--ws-hover); } }
|
|
11
|
+
button.ws-inline-edit__value:active { background: var(--ws-hover-strong); }
|
|
12
|
+
/* No value yet: the placeholder's words, still the target. */
|
|
13
|
+
.ws-inline-edit__value[data-empty] { color: var(--color-text-placeholder); }
|
|
14
|
+
/* The pencil after the words (a ws-icon at the words' size, 16 at most, so it sits on their line): a hint, never the only
|
|
15
|
+
target (the whole box is). */
|
|
16
|
+
.ws-inline-edit__mark { width: min(1em, var(--ws-icon)); height: min(1em, var(--ws-icon)); margin-inline-start: var(--space-xxs); color: var(--color-text-muted); opacity: 0; transition: opacity var(--ws-duration-hover) var(--ws-ease-soft); }
|
|
17
|
+
@media (hover: hover) { button.ws-inline-edit__value:hover .ws-inline-edit__mark { opacity: 1; } }
|
|
18
|
+
button.ws-inline-edit__value:focus-visible .ws-inline-edit__mark { opacity: 1; }
|
|
19
|
+
@media (pointer: coarse) { .ws-inline-edit__mark { opacity: 1; } }
|
|
20
|
+
/* Editing: the same box as a field. A textarea grows with its words where the browser can (field-sizing). */
|
|
21
|
+
.ws-inline-edit__value:is(input, textarea) { background: var(--ws-field); box-shadow: var(--ws-shadow-hairline-in); }
|
|
22
|
+
/* The ring is drawn inside the box, so it never crosses a label or a line 4 above or below. */
|
|
23
|
+
.ws-inline-edit__value:focus-visible, .ws-inline-edit__value--typing { outline-offset: calc(var(--ws-focus-width) * -1); }
|
|
24
|
+
textarea.ws-inline-edit__value { resize: none; field-sizing: content; }
|
|
25
|
+
.ws-inline-edit__value:is(input, textarea)::placeholder { color: var(--color-text-placeholder); }
|
|
26
|
+
.ws-inline-edit__value:is(input, textarea)[aria-invalid="true"] { box-shadow: inset 0 0 0 var(--ws-line) var(--ws-word-needs); }
|
|
27
|
+
/* Touch: the box is 40 tall at least, at rest and editing alike (so it still does not jump), and the editing words are
|
|
28
|
+
16 at least: a field under 16 px makes iOS zoom the page. */
|
|
29
|
+
@media (pointer: coarse) {
|
|
30
|
+
.ws-inline-edit__value { min-height: var(--ws-touch); }
|
|
31
|
+
.ws-inline-edit__value:is(input, textarea) { font-size: max(1em, var(--ws-text-value)); }
|
|
32
|
+
}
|
|
33
|
+
/* The editing state drawn still, for a preview or a mockup: the focus ring a real focus draws. */
|
|
34
|
+
.ws-inline-edit__value--typing { outline: var(--ws-focus-width) solid var(--ws-focus-color); }
|