@kernhq/module-quire 0.10.7 → 0.10.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernhq/module-quire",
3
- "version": "0.10.7",
3
+ "version": "0.10.8",
4
4
  "description": "Kern Quire: collaborative documents, spaces and page trees",
5
5
  "homepage": "https://github.com/KernAIO/module-quire#readme",
6
6
  "license": "AGPL-3.0-only",
@@ -7,9 +7,15 @@ import { t } from '../../i18n.js'
7
7
  * A date, stored as an ISO string.
8
8
  *
9
9
  * The native picker rather than a hand-rolled calendar: it is localised, keyboard-operable and
10
- * reachable by a screen reader without any of that being written here. It is drawn through
11
- * `formatDate` when it is not being edited, so the table reads in the interface language rather
12
- * than in the browser's.
10
+ * reachable by a screen reader without any of that being written here. But a native
11
+ * `<input type="date">` draws its *value* in the **browser's** locale, not the page's, and nothing
12
+ * can restyle that — so a table left permanently in edit mode showed `08/21/2026` in Latin digits
13
+ * on a Persian screen, in a column beside one already counting `۱ ۲ ۳`. The one untranslated thing
14
+ * on the page, which is the shape this project treats as a defect rather than as polish.
15
+ *
16
+ * So the cell reads through `formatDate` whether or not it is editable, and the input appears when
17
+ * somebody goes to change it. Focus moves to it on the way in and the formatted text comes back on
18
+ * the way out, so a keyboard never lands on something it cannot see.
13
19
  */
14
20
  interface Props {
15
21
  value: unknown
@@ -36,6 +42,22 @@ const fieldValue = $derived.by(() => {
36
42
 
37
43
  const shown = $derived(iso ? (withTime ? formatDateTime(iso) : formatDate(iso)) : '')
38
44
 
45
+ /** Swapped for the native input only while somebody is actually changing the date. */
46
+ let editing = $state(false)
47
+ let field = $state<HTMLInputElement>()
48
+
49
+ /*
50
+ * Taking focus is not optional here. Replacing the button with the input would otherwise leave
51
+ * focus on a removed element, which the browser resolves by dropping it to `<body>` — a keyboard
52
+ * user tabs to a date, presses Enter and is returned to the top of the page.
53
+ */
54
+ $effect(() => {
55
+ if (editing && field) {
56
+ field.focus()
57
+ field.showPicker?.()
58
+ }
59
+ })
60
+
39
61
  const commit = (next: string) => {
40
62
  if (next === '') {
41
63
  if (iso) onchange(null)
@@ -47,14 +69,25 @@ const commit = (next: string) => {
47
69
  }
48
70
  </script>
49
71
 
50
- {#if editable}
72
+ {#if editable && editing}
51
73
  <input
74
+ bind:this={field}
52
75
  class="cell-input"
53
76
  type={withTime ? 'datetime-local' : 'date'}
54
77
  value={fieldValue}
55
78
  aria-label={name}
56
79
  onchange={(e) => commit(e.currentTarget.value)}
80
+ onblur={() => {
81
+ editing = false
82
+ }}
83
+ onkeydown={(e) => {
84
+ if (e.key === 'Escape' || e.key === 'Enter') editing = false
85
+ }}
57
86
  />
87
+ {:else if editable}
88
+ <button type="button" class="cell-open" aria-label={name} onclick={() => (editing = true)}>
89
+ {#if shown}{shown}{:else}<span class="muted">{t('db_cell_empty')}</span>{/if}
90
+ </button>
58
91
  {:else}
59
92
  <span class="cell-static" title={reason}>
60
93
  {#if shown}{shown}{:else}<span class="muted">{t('db_cell_empty')}</span>{/if}
@@ -83,6 +116,36 @@ const commit = (next: string) => {
83
116
  background: var(--kern-surface-raised);
84
117
  outline: none;
85
118
  }
119
+ /* Mirrors `.cell-input` so swapping one for the other does not move the row by a pixel. */
120
+ .cell-open {
121
+ display: block;
122
+ width: 100%;
123
+ min-width: 0;
124
+ min-height: 26px;
125
+ padding: 2px 6px;
126
+ margin-inline-start: -6px;
127
+ border: 1px solid transparent;
128
+ border-radius: var(--kern-r-sm);
129
+ background: none;
130
+ color: inherit;
131
+ font: inherit;
132
+ font-size: 13px;
133
+ text-align: start;
134
+ overflow: hidden;
135
+ text-overflow: ellipsis;
136
+ white-space: nowrap;
137
+ cursor: pointer;
138
+ }
139
+ .cell-open:hover {
140
+ background: var(--kern-surface-active);
141
+ }
142
+ /*
143
+ * The ring itself comes from the global `:focus-visible` rule in tokens.css — a box-shadow, not an
144
+ * outline. Only the ground is set here, so the two do not fight.
145
+ */
146
+ .cell-open:focus-visible {
147
+ background: var(--kern-surface-raised);
148
+ }
86
149
  .cell-static {
87
150
  min-width: 0;
88
151
  overflow: hidden;