@gjermundgaraba-designs/pixel-paper 0.23.0 → 0.24.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.
@@ -1,5 +1,5 @@
1
1
  import { brandViewResult as __foldkitBrandViewResult } from 'foldkit/brand'
2
- import { Array, Duration, Effect, Match, Number, Option, Schema, Stream, pipe } from "effect";
2
+ import { Array, Duration, Effect, Equal, Match, Number, Option, Schema, Stream, pipe, } from "effect";
3
3
  import * as Command from "foldkit/command";
4
4
  import * as Dom from "foldkit/dom";
5
5
  import { defineMessageUnion } from "foldkit/message";
@@ -15,7 +15,9 @@ import { prefixKeys } from "../subscription/prefixKeys.js";
15
15
  const Side = Schema.Literals(["Top", "Right"]);
16
16
  /** What brought the tip: a resting pointer or keyboard focus. */
17
17
  export const Trigger = Schema.Literals(["Pointer", "Focus"]);
18
- const Words = { text: Schema.String, side: Side };
18
+ /** What a tip says: its control's name, and the key that does the same where the control
19
+ * names one (`data-tip-key`). */
20
+ const Words = { text: Schema.String, maybeKey: Schema.Option(Schema.String), side: Side };
19
21
  const ShownWords = { ...Words, trigger: Trigger };
20
22
  /** Where the page's tip is. `Cold`: none lately. `Waiting`: a pointer rests on a control, for
21
23
  * a moment first. `Shown`. `Leaving`: the pointer left, with a moment to cross onto the tip.
@@ -37,10 +39,10 @@ export const Model = Schema.Struct({
37
39
  // MESSAGE
38
40
  /** Union of all Messages the tip layer can produce. */
39
41
  export const Message = defineMessageUnion({
40
- EnteredTipTarget: { text: Schema.String, side: Side },
42
+ EnteredTipTarget: Words,
41
43
  LeftTipTarget: {},
42
44
  EnteredTip: {},
43
- FocusedTipTarget: { text: Schema.String, side: Side },
45
+ FocusedTipTarget: Words,
44
46
  BlurredTipTarget: {},
45
47
  PressedPointer: {},
46
48
  PressedKey: {},
@@ -70,6 +72,8 @@ const GRACE = Duration.millis(120);
70
72
  const WARMTH = Duration.millis(400);
71
73
  /** The attribute that holds a control's tip: its accessible name. */
72
74
  const TIP_ATTRIBUTE = "data-tip";
75
+ /** The attribute that holds the key a control's tip prints after its name. */
76
+ const TIP_KEY_ATTRIBUTE = "data-tip-key";
73
77
  const TIP_SELECTOR = `[${TIP_ATTRIBUTE}]`;
74
78
  const hasText = (element, text) => __foldkitBrandViewResult((element.isConnected && element.getAttribute(TIP_ATTRIBUTE) === text), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#hasText");
75
79
  /** The control a tip belongs to, found when it opens: the innermost hovered element with
@@ -117,9 +121,10 @@ export const WaitBeforeCooling = Command.define("WaitBeforeCooling", {
117
121
  messages: [Message.CompletedWaitBeforeCooling],
118
122
  execute: ({ version }) => __foldkitBrandViewResult((Effect.sleep(WARMTH).pipe(Effect.as(Message.CompletedWaitBeforeCooling({ version })))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#execute~5"),
119
123
  });
120
- const show = (model, text, side, trigger) => (__foldkitBrandViewResult(({
121
- model: modifyFields(model, { state: () => __foldkitBrandViewResult((TipState.Shown({ text, side, trigger })), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#state") }),
122
- commands: [ShowTip({ tipId: model.id, text, trigger })],
124
+ const wordsOf = ({ text, maybeKey, side }) => (__foldkitBrandViewResult(({ text, maybeKey, side }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#wordsOf"));
125
+ const show = (model, words, trigger) => (__foldkitBrandViewResult(({
126
+ model: modifyFields(model, { state: () => __foldkitBrandViewResult((TipState.Shown({ ...wordsOf(words), trigger })), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#state") }),
127
+ commands: [ShowTip({ tipId: model.id, text: words.text, trigger })],
123
128
  }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#show"));
124
129
  const hide = (model) => {
125
130
  const nextVersion = Number.increment(model.version);
@@ -128,12 +133,12 @@ const hide = (model) => {
128
133
  commands: [HideTip({ tipId: model.id }), WaitBeforeCooling({ version: nextVersion })],
129
134
  }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#hide");
130
135
  };
131
- const wait = (model, text, side) => {
136
+ const wait = (model, words) => {
132
137
  const nextVersion = Number.increment(model.version);
133
138
  return __foldkitBrandViewResult(({
134
139
  model: modifyFields(model, {
135
140
  version: () => __foldkitBrandViewResult((nextVersion), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#version~2"),
136
- state: () => __foldkitBrandViewResult((TipState.Waiting({ text, side })), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#state~3"),
141
+ state: () => __foldkitBrandViewResult((TipState.Waiting(wordsOf(words))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#state~3"),
137
142
  }),
138
143
  commands: [WaitBeforeShowing({ version: nextVersion })],
139
144
  }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#wait");
@@ -143,7 +148,7 @@ const leave = (model, shown) => {
143
148
  return __foldkitBrandViewResult(({
144
149
  model: modifyFields(model, {
145
150
  version: () => __foldkitBrandViewResult((nextVersion), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#version~3"),
146
- state: () => __foldkitBrandViewResult((TipState.Leaving({ text: shown.text, side: shown.side, trigger: shown.trigger })), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#state~4"),
151
+ state: () => __foldkitBrandViewResult((TipState.Leaving({ ...wordsOf(shown), trigger: shown.trigger })), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#state~4"),
147
152
  }),
148
153
  commands: [WaitBeforeHiding({ version: nextVersion })],
149
154
  }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#leave");
@@ -159,47 +164,55 @@ const dismiss = (model) => __foldkitBrandViewResult((TipState.match(model.state,
159
164
  Leaving: () => __foldkitBrandViewResult((hide(model)), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Leaving"),
160
165
  Warm: () => (__foldkitBrandViewResult(({ model }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Warm")),
161
166
  })), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#dismiss");
162
- const entered = (model, text, side) => __foldkitBrandViewResult((TipState.match(model.state, {
163
- Cold: () => __foldkitBrandViewResult((wait(model, text, side)), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Cold~2"),
164
- Waiting: (waiting) => (__foldkitBrandViewResult((waiting.text === text ? { model } : wait(model, text, side)), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Waiting~2")),
165
- Shown: (shown) => (__foldkitBrandViewResult((shown.text === text ? { model } : show(model, text, side, "Pointer")), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Shown~2")),
166
- Leaving: (leaving) => __foldkitBrandViewResult((leaving.text === text
167
+ const entered = (model, words) => __foldkitBrandViewResult((TipState.match(model.state, {
168
+ Cold: () => __foldkitBrandViewResult((wait(model, words)), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Cold~2"),
169
+ Waiting: (waiting) => (__foldkitBrandViewResult((waiting.text === words.text ? { model } : wait(model, words)), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Waiting~2")),
170
+ Shown: (shown) => (__foldkitBrandViewResult((shown.text === words.text ? { model } : show(model, words, "Pointer")), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Shown~2")),
171
+ Leaving: (leaving) => __foldkitBrandViewResult((leaving.text === words.text
167
172
  ? {
168
173
  model: modifyFields(model, {
169
- state: () => __foldkitBrandViewResult((TipState.Shown({ text, side, trigger: leaving.trigger })), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#state~6"),
174
+ state: () => __foldkitBrandViewResult((TipState.Shown({ ...wordsOf(words), trigger: leaving.trigger })), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#state~6"),
170
175
  }),
171
176
  }
172
- : show(model, text, side, "Pointer")), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Leaving~2"),
173
- Warm: () => __foldkitBrandViewResult((show(model, text, side, "Pointer")), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Warm~2"),
177
+ : show(model, words, "Pointer")), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Leaving~2"),
178
+ Warm: () => __foldkitBrandViewResult((show(model, words, "Pointer")), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Warm~2"),
174
179
  })), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#entered");
175
180
  /** Processes a tip layer Message and returns the next Model and Commands. */
176
181
  export const update = (model, message) => __foldkitBrandViewResult((Message.match(message, {
177
- EnteredTipTarget: ({ text, side }) => __foldkitBrandViewResult((entered(model, text, side)), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#EnteredTipTarget"),
182
+ EnteredTipTarget: (words) => __foldkitBrandViewResult((entered(model, wordsOf(words))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#EnteredTipTarget"),
178
183
  LeftTipTarget: () => __foldkitBrandViewResult((TipState.matchOrElse(model.state, {
179
184
  Waiting: () => __foldkitBrandViewResult((cool(model)), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Waiting~3"),
180
185
  Shown: (shown) => (__foldkitBrandViewResult((shown.trigger === "Pointer" ? leave(model, shown) : { model }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Shown~3")),
181
186
  }, () => (__foldkitBrandViewResult(({ model }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~11")))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#LeftTipTarget"),
182
187
  EnteredTip: () => __foldkitBrandViewResult((TipState.matchOrElse(model.state, {
183
- Leaving: ({ text, side, trigger }) => (__foldkitBrandViewResult(({
184
- model: modifyFields(model, { state: () => __foldkitBrandViewResult((TipState.Shown({ text, side, trigger })), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#state~7") }),
188
+ Leaving: (leaving) => (__foldkitBrandViewResult(({
189
+ model: modifyFields(model, {
190
+ state: () => __foldkitBrandViewResult((TipState.Shown({ ...wordsOf(leaving), trigger: leaving.trigger })), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#state~7"),
191
+ }),
185
192
  }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Leaving~3")),
186
193
  }, () => (__foldkitBrandViewResult(({ model }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~12")))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#EnteredTip"),
187
- FocusedTipTarget: ({ text, side }) => __foldkitBrandViewResult((show(model, text, side, "Focus")), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#FocusedTipTarget"),
194
+ FocusedTipTarget: (words) => __foldkitBrandViewResult((show(model, wordsOf(words), "Focus")), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#FocusedTipTarget"),
188
195
  BlurredTipTarget: () => __foldkitBrandViewResult((dismiss(model)), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#BlurredTipTarget"),
189
196
  PressedPointer: () => __foldkitBrandViewResult((dismiss(model)), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#PressedPointer"),
190
197
  PressedKey: () => __foldkitBrandViewResult((dismiss(model)), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#PressedKey"),
191
198
  CompletedWaitBeforeShowing: ({ version }) => __foldkitBrandViewResult((TipState.matchOrElse(model.state, {
192
- Waiting: ({ text, side }) => __foldkitBrandViewResult((model.version === version ? show(model, text, side, "Pointer") : { model }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Waiting~4"),
199
+ Waiting: (waiting) => __foldkitBrandViewResult((model.version === version ? show(model, wordsOf(waiting), "Pointer") : { model }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Waiting~4"),
193
200
  }, () => (__foldkitBrandViewResult(({ model }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~13")))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#CompletedWaitBeforeShowing"),
194
201
  CompletedWaitBeforeHiding: ({ version }) => __foldkitBrandViewResult((TipState.matchOrElse(model.state, { Leaving: () => (__foldkitBrandViewResult((model.version === version ? hide(model) : { model }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Leaving~4")) }, () => (__foldkitBrandViewResult(({ model }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~14")))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#CompletedWaitBeforeHiding"),
195
202
  CompletedWaitBeforeCooling: ({ version }) => __foldkitBrandViewResult((TipState.matchOrElse(model.state, { Warm: () => (__foldkitBrandViewResult((model.version === version ? cool(model) : { model }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#Warm~3")) }, () => (__foldkitBrandViewResult(({ model }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~15")))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#CompletedWaitBeforeCooling"),
196
203
  CompletedShowTip: () => (__foldkitBrandViewResult(({ model }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#CompletedShowTip")),
197
204
  CompletedHideTip: () => (__foldkitBrandViewResult(({ model }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#CompletedHideTip")),
198
205
  })), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#update");
199
- const sideOf = (element) => __foldkitBrandViewResult((element.closest('[aria-orientation="vertical"]') === null ? "Top" : "Right"), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#sideOf");
206
+ /** A control in a vertical strip of tools, or set in vertical writing (a folded rail's title
207
+ * bar), takes its tip at its side, where the tip would cover the next tool or the top bar. */
208
+ const sideOf = (element) => __foldkitBrandViewResult((element.closest('[aria-orientation="vertical"]') === null &&
209
+ !getComputedStyle(element).writingMode.startsWith("vertical")
210
+ ? "Top"
211
+ : "Right"), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#sideOf");
200
212
  const tipTargetOf = (target) => __foldkitBrandViewResult((pipe(Option.liftPredicate(target, (node) => __foldkitBrandViewResult((node instanceof Element), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~16")), Option.flatMap((element) => __foldkitBrandViewResult((Option.fromNullishOr(element.closest(TIP_SELECTOR))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~17")), Option.flatMap((element) => __foldkitBrandViewResult((Option.map(Option.fromNullishOr(element.getAttribute(TIP_ATTRIBUTE)), (text) => (__foldkitBrandViewResult(({
201
213
  element,
202
214
  text,
215
+ maybeKey: Option.fromNullishOr(element.getAttribute(TIP_KEY_ATTRIBUTE)),
203
216
  side: sideOf(element),
204
217
  }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~19")))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~18")))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#tipTargetOf");
205
218
  /** What a pointer arriving over `target` means for the tip: over a control with a tip, over
@@ -214,12 +227,12 @@ export const pointerOverMessage = (tipId, event) => {
214
227
  }
215
228
  return __foldkitBrandViewResult((Option.some(Option.match(tipTargetOf(event.target), {
216
229
  onNone: () => __foldkitBrandViewResult((Message.LeftTipTarget()), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#onNone"),
217
- onSome: ({ text, side }) => __foldkitBrandViewResult((Message.EnteredTipTarget({ text, side })), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#onSome"),
230
+ onSome: (target) => __foldkitBrandViewResult((Message.EnteredTipTarget(wordsOf(target))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#onSome"),
218
231
  }))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#pointerOverMessage");
219
232
  };
220
233
  /** What focus arriving on `target` means for the tip: keyboard focus (`:focus-visible`) on a
221
234
  * control with a tip shows it; any other focus says nothing. */
222
- const focusInMessage = (event) => __foldkitBrandViewResult((Option.map(Option.filter(tipTargetOf(event.target), ({ element }) => __foldkitBrandViewResult((element.matches(":focus-visible")), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~20")), ({ text, side }) => __foldkitBrandViewResult((Message.FocusedTipTarget({ text, side })), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~21"))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#focusInMessage");
235
+ const focusInMessage = (event) => __foldkitBrandViewResult((Option.map(Option.filter(tipTargetOf(event.target), ({ element }) => __foldkitBrandViewResult((element.matches(":focus-visible")), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~20")), (target) => __foldkitBrandViewResult((Message.FocusedTipTarget(wordsOf(target))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~21"))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#focusInMessage");
223
236
  /** Focus leaving a control with a tip, not into a part of itself, closes the tip (and
224
237
  * cancels one still waiting). */
225
238
  export const focusOutMessage = (event) => __foldkitBrandViewResult((Option.map(Option.filter(tipTargetOf(event.target), ({ element }) => __foldkitBrandViewResult((!(event.relatedTarget instanceof Node && element.contains(event.relatedTarget))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~22")), () => __foldkitBrandViewResult((Message.BlurredTipTarget()), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~23"))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#focusOutMessage");
@@ -244,7 +257,9 @@ const pointerOverChanges = (tipId) => __foldkitBrandViewResult((Dom.streamFromEv
244
257
  }).pipe(Stream.changesWith((previous, next) => __foldkitBrandViewResult((previous._tag === next._tag && sameWords(previous, next)), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~24")))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#pointerOverChanges");
245
258
  const sameWords = (previous, next) => __foldkitBrandViewResult((previous._tag !== "EnteredTipTarget" ||
246
259
  next._tag !== "EnteredTipTarget" ||
247
- (previous.text === next.text && previous.side === next.side)), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#sameWords");
260
+ (previous.text === next.text &&
261
+ previous.side === next.side &&
262
+ Equal.equals(previous.maybeKey, next.maybeKey))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#sameWords");
248
263
  const tipSubscriptions = Subscription.make()((entry) => (__foldkitBrandViewResult(({
249
264
  pointerAndFocus: entry({ tipId: Schema.String }, {
250
265
  modelToDependencies: (model) => (__foldkitBrandViewResult(({ tipId: model.id }), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#modelToDependencies")),
@@ -286,10 +301,11 @@ const tipSubscriptions = Subscription.make()((entry) => (__foldkitBrandViewResul
286
301
  * only while a tip shows. */
287
302
  export const subscriptions = (keyPrefix) => __foldkitBrandViewResult((prefixKeys(keyPrefix, tipSubscriptions)), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#subscriptions");
288
303
  // VIEW
289
- const wordsOf = (model) => __foldkitBrandViewResult((Match.value(model.state).pipe(Match.withReturnType(), Match.tag("Waiting", "Shown", "Leaving", ({ text, side }) => __foldkitBrandViewResult((Option.some({ text, side })), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~26")), Match.orElse(() => __foldkitBrandViewResult((Option.none()), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~27")))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#wordsOf");
304
+ const maybeWordsOf = (model) => __foldkitBrandViewResult((Match.value(model.state).pipe(Match.withReturnType(), Match.tag("Waiting", "Shown", "Leaving", (words) => __foldkitBrandViewResult((Option.some(wordsOf(words))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~26")), Match.orElse(() => __foldkitBrandViewResult((Option.none()), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~27")))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#maybeWordsOf");
290
305
  /**
291
306
  * The page's one tip: `div.tip[popover="manual"][aria-hidden="true"]`, which shows the words
292
- * of any control carrying `data-tip` (IconButton, the glyph switch, a board's grip). Render it
307
+ * of any control carrying `data-tip` (IconButton, the glyph switch, a board's grip), then the
308
+ * key in its `data-tip-key` where it has one (an IconButton's `keyHint`). Render it
293
309
  * once, beside the Announcer, and lift `subscriptions(keyPrefix)`. It opens on a resting
294
310
  * pointer after a moment (at once while another has just closed), never on touch, and on
295
311
  * keyboard focus; it can be hovered; leaving, blur, a press or any key closes it, and Escape
@@ -301,7 +317,7 @@ const wordsOf = (model) => __foldkitBrandViewResult((Match.value(model.state).pi
301
317
  * accessibility tree, and any control takes one by carrying `data-tip`.
302
318
  */
303
319
  export const view = defineView((model, h) => {
304
- const maybeWords = wordsOf(model);
320
+ const maybeWords = maybeWordsOf(model);
305
321
  return __foldkitBrandViewResult((h.div([
306
322
  h.Id(model.id),
307
323
  h.Popover("manual"),
@@ -310,5 +326,11 @@ export const view = defineView((model, h) => {
310
326
  ...(Option.exists(maybeWords, ({ side }) => __foldkitBrandViewResult((side === "Right"), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~29"))
311
327
  ? [h.DataAttribute("side", "right")]
312
328
  : []),
313
- ], Option.isSome(maybeWords) ? [maybeWords.value.text] : [])), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~28");
329
+ ], Option.match(maybeWords, {
330
+ onNone: () => __foldkitBrandViewResult(([]), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#onNone~3"),
331
+ onSome: ({ text, maybeKey }) => __foldkitBrandViewResult(([
332
+ text,
333
+ ...Array.fromOption(Option.map(maybeKey, (key) => __foldkitBrandViewResult((h.kbd([h.Class("key-hint")], [key])), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~30"))),
334
+ ]), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#onSome~3"),
335
+ }))), "@gjermundgaraba-designs/pixel-paper/src/tips/tips.js#anonymous~28");
314
336
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjermundgaraba-designs/pixel-paper",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -2,8 +2,10 @@ import { Button as UiButton } from "@foldkit/ui";
2
2
  import type { Html, HtmlBuilder } from "foldkit/html";
3
3
 
4
4
  import type { Attributes } from "../attributes/attributes.ts";
5
+ import type { KeyHintConfig } from "../button/button.ts";
5
6
  import * as Glyph from "../glyph/glyph.ts";
6
7
  import type { GlyphName } from "../glyph/glyphs.ts";
8
+ import { keyLabel } from "../keyHint/keyLabel.ts";
7
9
 
8
10
  // VIEW
9
11
 
@@ -17,13 +19,25 @@ export type ViewConfig<Message> = Readonly<{
17
19
  isQuiet?: boolean;
18
20
  /** Unavailable but focusable (`aria-disabled`), as in `@foldkit/ui`. */
19
21
  isDisabled?: boolean;
22
+ /** The key that does the same, printed in the tip after the label (`data-tip-key`): the
23
+ * button has no words to print it beside. */
24
+ keyHint?: KeyHintConfig;
20
25
  /** Further attributes for the `<button>`, never a class. */
21
26
  attributes?: Attributes<Message>;
22
27
  }>;
23
28
 
24
- /** A bare glyph button, 24px square. Its label is its accessible name and its tip. */
29
+ /** A bare glyph button, 24px square. Its label is its accessible name and its tip, and its
30
+ * key, where it has one, prints in the tip after the label. */
25
31
  export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Message>): Html => {
26
- const { glyph, label, onClick, isQuiet = false, isDisabled = false, attributes = [] } = config;
32
+ const {
33
+ glyph,
34
+ label,
35
+ onClick,
36
+ isQuiet = false,
37
+ isDisabled = false,
38
+ keyHint,
39
+ attributes = [],
40
+ } = config;
27
41
 
28
42
  return UiButton.view(
29
43
  {
@@ -36,6 +50,9 @@ export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Messag
36
50
  ...groups.button,
37
51
  h.AriaLabel(label),
38
52
  h.DataAttribute("tip", label),
53
+ ...(keyHint === undefined
54
+ ? []
55
+ : [h.DataAttribute("tip-key", keyLabel(keyHint.keys, keyHint.platform))]),
39
56
  h.Class(isQuiet ? "iconbtn quiet" : "iconbtn"),
40
57
  ],
41
58
  [Glyph.view({ name: glyph }, h)],
@@ -26,6 +26,9 @@ export type ViewConfig = Readonly<{
26
26
  /** Small Buttons and IconButtons at the title bar's right end, after the meta. On the ink
27
27
  * bar they print in paper, flat, and ring in their own colour inside their edge. */
28
28
  actions?: ReadonlyArray<Html | string>;
29
+ /** A strip on the head tint between the title bar and the body, for a field that picks what
30
+ * the body shows (an area over its tasks). Its labels print in on-head. */
31
+ shelf?: ReadonlyArray<Html | string>;
29
32
  body: ReadonlyArray<Html | string>;
30
33
  /** A ruled foot under the body: a note line, then actions pushed right. */
31
34
  foot?: ReadonlyArray<Html | string>;
@@ -46,6 +49,7 @@ export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html
46
49
  lead = [],
47
50
  meta,
48
51
  actions = [],
52
+ shelf = [],
49
53
  body,
50
54
  foot = [],
51
55
  isFlush = false,
@@ -68,6 +72,7 @@ export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html
68
72
  ...slotView("span", "panel-acts", actions, h),
69
73
  ],
70
74
  ),
75
+ ...slotView("div", "panel-shelf", shelf, h),
71
76
  ...(isFlush ? body : [h.div([h.Class("panel-body")], body)]),
72
77
  ...slotView("div", "panel-foot", foot, h),
73
78
  ],
@@ -979,6 +979,19 @@ a.textbtn {
979
979
  flex: 1 1 100%;
980
980
  }
981
981
 
982
+ /* the shelf: a strip on the head tint under the bar, for a field that picks what the body
983
+ shows */
984
+ .panel-shelf {
985
+ padding: 10px 12px 12px;
986
+ background: var(--head);
987
+ border-bottom: var(--hair) solid var(--rule);
988
+ }
989
+
990
+ /* on the head tint a field's label takes on-head: muted misses AA there */
991
+ .panel-shelf :is(.field > label:not(.choice), .field-label) {
992
+ color: var(--on-head);
993
+ }
994
+
982
995
  /* ---------- the panel's title-bar lead and actions ---------- */
983
996
 
984
997
  /* small buttons and icon buttons at the bar's right end, and an icon button before the
@@ -4828,6 +4841,16 @@ dialog.veil:focus {
4828
4841
  overflow: visible;
4829
4842
  }
4830
4843
 
4844
+ /* the key that does the same as the control, after its words, as a button prints it */
4845
+ .tip .key-hint {
4846
+ margin-left: 6px;
4847
+ font-family: var(--font-px);
4848
+ font-size: var(--type-label);
4849
+ text-transform: none;
4850
+ letter-spacing: 0;
4851
+ opacity: var(--fade-on-ink);
4852
+ }
4853
+
4831
4854
  /* ---------- toolbars: one strip of tools, one stop for Tab ---------- */
4832
4855
 
4833
4856
  /* the rules between tools are the strip's ink showing through a 2px gap */
@@ -5022,8 +5045,7 @@ dialog.veil:focus {
5022
5045
  /* ---------- the rail: a column of panels beside the deck, a drawer on a phone ---------- */
5023
5046
 
5024
5047
  /* one element in two places: at rest, fixed down the left edge from the top bar's lower
5025
- edge (as the stage hangs) and scrolling itself; on a phone the same element opens as a
5026
- popover drawer. The reader's width, when they gave one, is --railed-width, which the view
5048
+ edge (as the stage hangs); on a phone the same element opens as a popover drawer. The reader's width, when they gave one, is --railed-width, which the view
5027
5049
  sets on .railed, so the page's padding and the rail's width follow it together; without one,
5028
5050
  --rail. It is not inherited: each box it sizes takes it from .railed by name, so a width that
5029
5051
  changes under a dragged splitter restyles those four and none of the rows inside them */
@@ -5046,8 +5068,8 @@ dialog.veil:focus {
5046
5068
  top: anchor(--topbar bottom, 0px);
5047
5069
  bottom: var(--statusbar-h);
5048
5070
  left: 0;
5049
- display: grid;
5050
- align-content: start;
5071
+ display: flex;
5072
+ flex-direction: column;
5051
5073
  gap: var(--gap);
5052
5074
  width: var(--railed-width, var(--rail));
5053
5075
  height: auto;
@@ -5059,9 +5081,45 @@ dialog.veil:focus {
5059
5081
  }
5060
5082
 
5061
5083
  .rail > * {
5084
+ flex: none;
5062
5085
  min-width: 0;
5063
5086
  }
5064
5087
 
5088
+ /* the rail's top stays put: what comes before its last panel keeps its height, and the last
5089
+ panel takes the room left, at most its own height, its bar, shelf and foot staying while
5090
+ its body scrolls (the padded body, or a flush body's list). The rail scrolls as a whole
5091
+ only when what comes before that panel is taller than the column */
5092
+ .rail > .panel:last-child {
5093
+ display: flex;
5094
+ flex-direction: column;
5095
+ flex: 0 1 auto;
5096
+ min-height: 0;
5097
+ }
5098
+
5099
+ .rail > .panel:last-child > :is(.panel-tb, .panel-shelf, .panel-foot) {
5100
+ flex: none;
5101
+ }
5102
+
5103
+ .rail > .panel:last-child > :not(.panel-tb, .panel-shelf, .panel-foot) {
5104
+ flex: 0 1 auto;
5105
+ min-height: 0;
5106
+ overflow-y: auto;
5107
+ }
5108
+
5109
+ /* a long name ends short of the bar's tools rather than wrapping the bar taller */
5110
+ .rail .panel-tb h2 {
5111
+ min-width: 0;
5112
+ overflow: hidden;
5113
+ overflow-wrap: normal;
5114
+ white-space: nowrap;
5115
+ text-overflow: ellipsis;
5116
+ }
5117
+
5118
+ /* the toggle is the button itself in the bar's lead */
5119
+ .rail-toggle {
5120
+ display: contents;
5121
+ }
5122
+
5065
5123
  /* the splitter on the rail's right edge: a 12px strip from the edge outward, over the page's
5066
5124
  margin and never over the rail's own scrollbar, and beside the rail rather than in it (the
5067
5125
  rail scrolls, and its drawer's first Tab stop is its first control). It shows itself as a
@@ -5102,6 +5160,48 @@ dialog.veil:focus {
5102
5160
  display: none;
5103
5161
  }
5104
5162
 
5163
+ /* folded: the first panel stands its title bar on end on the ground where the rail stood, the
5164
+ toggle at its top and the title and count reading down; the rest of the rail and the
5165
+ splitter are gone, and the deck takes the width. Above 900px only: the drawer opens whole.
5166
+ Folding and unfolding move in three steps, as the drawer enters, but never while a pointer
5167
+ drags the splitter, where the rail follows the pointer */
5168
+ @media not (max-width: 900px) {
5169
+ .railed.folded {
5170
+ --railed-width: var(--rail-folded);
5171
+ }
5172
+
5173
+ .railed.folded > .rail > :not(.rail-head + .panel),
5174
+ .railed.folded > .rail > .rail-head + .panel > :not(.panel-tb),
5175
+ .railed.folded > .rail > .rail-head + .panel > .panel-tb > .panel-acts {
5176
+ display: none;
5177
+ }
5178
+
5179
+ .railed.folded > .rail > .rail-head + .panel {
5180
+ align-self: flex-start;
5181
+ }
5182
+
5183
+ .railed.folded > .rail > .rail-head + .panel > .panel-tb {
5184
+ writing-mode: vertical-rl;
5185
+ min-width: 31px;
5186
+ min-height: 0;
5187
+ padding: 3px 3px 12px;
5188
+ }
5189
+
5190
+ .railed.folded > .rail > .rail-head + .panel > .panel-tb > .meta {
5191
+ margin: 0;
5192
+ }
5193
+
5194
+ .railed,
5195
+ .railed > :is(.rail, .bench) {
5196
+ transition: var(--motion-card) steps(3, end);
5197
+ transition-property: padding-left, width, left;
5198
+ }
5199
+
5200
+ :root[data-pixel-paper-dragging] :is(.railed, .railed > .rail, .railed > .bench) {
5201
+ transition: none;
5202
+ }
5203
+ }
5204
+
5105
5205
  @media (max-width: 900px) {
5106
5206
  .railed {
5107
5207
  padding-left: 0;
@@ -5140,6 +5240,16 @@ dialog.veil:focus {
5140
5240
  display: inline-flex;
5141
5241
  justify-self: start;
5142
5242
  }
5243
+
5244
+ /* the drawer opens and closes but never folds, so it prints no toggle, and the bar the
5245
+ toggle led keeps its own padding */
5246
+ .panel-lead:has(> .rail-toggle) {
5247
+ display: none;
5248
+ }
5249
+
5250
+ .panel-tb:has(> .panel-lead > .rail-toggle) {
5251
+ padding-left: 12px;
5252
+ }
5143
5253
  }
5144
5254
 
5145
5255
  @keyframes railIn {
@@ -5842,4 +5952,9 @@ dialog.veil:focus {
5842
5952
  .toasts .toast {
5843
5953
  animation: none;
5844
5954
  }
5955
+
5956
+ .railed,
5957
+ .railed > :is(.rail, .bench) {
5958
+ transition: none;
5959
+ }
5845
5960
  }
package/src/rail/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export {
2
+ FocusRailToggle,
2
3
  FollowRailDrawer,
3
4
  HideRailDrawer,
4
5
  Message,
@@ -10,6 +11,7 @@ export {
10
11
  init,
11
12
  isOpen,
12
13
  subscriptions,
14
+ toggle,
13
15
  update,
14
16
  view,
15
17
  } from "./rail.ts";