@dorsk/tsumikit 0.79.0 → 0.80.1

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.
Files changed (53) hide show
  1. package/dist/components/atoms/Artwork.svelte +14 -10
  2. package/dist/components/atoms/Artwork.svelte.d.ts +1 -1
  3. package/dist/components/atoms/Badge.svelte +1 -4
  4. package/dist/components/atoms/Button.svelte +303 -9
  5. package/dist/components/atoms/Button.svelte.d.ts +4 -1
  6. package/dist/components/atoms/Card.svelte +142 -0
  7. package/dist/components/atoms/Checkbox.svelte +177 -1
  8. package/dist/components/atoms/Input.svelte +35 -0
  9. package/dist/components/atoms/Link.svelte +61 -0
  10. package/dist/components/atoms/Progress.svelte +74 -1
  11. package/dist/components/atoms/Progress.svelte.d.ts +1 -0
  12. package/dist/components/atoms/Select.svelte +30 -0
  13. package/dist/components/atoms/Skeleton.svelte +54 -3
  14. package/dist/components/atoms/Skeleton.svelte.d.ts +1 -0
  15. package/dist/components/atoms/Spinner.svelte +152 -3
  16. package/dist/components/atoms/Spinner.svelte.d.ts +1 -1
  17. package/dist/components/atoms/Switch.svelte +135 -0
  18. package/dist/components/atoms/Textarea.svelte +36 -0
  19. package/dist/components/atoms/Thinking.svelte +130 -0
  20. package/dist/components/atoms/Thinking.svelte.d.ts +15 -0
  21. package/dist/components/molecules/CopyButton.svelte +146 -5
  22. package/dist/components/molecules/Field.svelte +55 -1
  23. package/dist/components/molecules/FileButton.svelte +4 -2
  24. package/dist/components/molecules/InputGroup.svelte +1 -1
  25. package/dist/components/molecules/LookPicker.svelte +100 -0
  26. package/dist/components/molecules/LookPicker.svelte.d.ts +17 -0
  27. package/dist/components/molecules/MotionSettings.svelte +166 -0
  28. package/dist/components/molecules/MotionSettings.svelte.d.ts +18 -0
  29. package/dist/components/molecules/RadioGroup.svelte +61 -0
  30. package/dist/components/molecules/SectionHeader.svelte +2 -0
  31. package/dist/components/molecules/Tabs.svelte +157 -4
  32. package/dist/components/molecules/ThemePicker.svelte +8 -3
  33. package/dist/fx.d.ts +149 -0
  34. package/dist/fx.js +111 -0
  35. package/dist/fx.md +58 -0
  36. package/dist/index.d.ts +8 -2
  37. package/dist/index.js +8 -2
  38. package/dist/looks/README.md +101 -0
  39. package/dist/looks/depth.css +939 -0
  40. package/dist/looks/editorial.css +545 -0
  41. package/dist/looks/tactile.css +871 -0
  42. package/dist/markdown.js +47 -21
  43. package/dist/pointer.d.ts +3 -0
  44. package/dist/pointer.js +40 -0
  45. package/dist/stores/fx.svelte.d.ts +17 -0
  46. package/dist/stores/fx.svelte.js +64 -0
  47. package/dist/stores/theme.svelte.d.ts +8 -2
  48. package/dist/stores/theme.svelte.js +33 -7
  49. package/dist/styles/themes.css +249 -249
  50. package/dist/styles/tokens.css +4 -3
  51. package/dist/theme-mode.d.ts +31 -0
  52. package/dist/theme-mode.js +19 -0
  53. package/package.json +3 -2
package/dist/markdown.js CHANGED
@@ -89,7 +89,9 @@ function inline(s, autolink) {
89
89
  function splitRow(row) {
90
90
  const cells = [];
91
91
  let cur = '';
92
- const t = row.trim().replace(/^\|/, '').replace(/\|$/, '');
92
+ let t = row.trim();
93
+ if (t.startsWith('|'))
94
+ t = t.slice(1);
93
95
  for (let i = 0; i < t.length; i++) {
94
96
  if (t[i] === '\\' && t[i + 1] === '|') {
95
97
  cur += '|';
@@ -102,32 +104,57 @@ function splitRow(row) {
102
104
  else
103
105
  cur += t[i];
104
106
  }
105
- cells.push(cur);
107
+ if (cur.trim() || !cells.length)
108
+ cells.push(cur);
106
109
  return cells;
107
110
  }
108
- const TABLE_RE = /(?:^|\n)([ \t]*\|.+\|[ \t]*)\n([ \t]*\|(?:[ \t]*:?-+:?[ \t]*\|)+[ \t]*)\n((?:[ \t]*\|.*\|[ \t]*(?:\n|$))+)/g;
111
+ const hasPipe = (line) => /(?:^|[^\\])\|/.test(line);
112
+ const DELIM_CELL = /^\s*:?-+:?\s*$/;
113
+ const TABLE_BREAK = new RegExp(`^\\s*(?:$|#{1,6}(?:\\s|$)|&gt;|(?:[-*+]|\\d{1,9}[.)])\\s|(?:-{3,}|\\*{3,}|_{3,})\\s*$|${BLOCK_L}\\d+${BLOCK_R}\\s*$)`);
109
114
  function table(header, delim, body, autolink) {
110
- const aligns = splitRow(delim).map((c) => {
115
+ const aligns = delim.map((c) => {
111
116
  const l = c.trim().startsWith(':');
112
117
  const r = c.trim().endsWith(':');
113
118
  return r && l ? 'center' : r ? 'right' : l ? 'left' : '';
114
119
  });
115
- const cell = (txt, i, tag) => {
116
- const a = aligns[i] ? ` style="text-align:${aligns[i]}"` : '';
117
- return `<${tag}${a}>${inline(txt.trim(), autolink)}</${tag}>`;
120
+ const row = (line, tag) => {
121
+ const cells = splitRow(line);
122
+ const html = aligns.map((a, i) => {
123
+ const style = a ? ` style="text-align:${a}"` : '';
124
+ return `<${tag}${style}>${inline((cells[i] ?? '').trim(), autolink)}</${tag}>`;
125
+ });
126
+ return `<tr>${html.join('')}</tr>`;
118
127
  };
119
- const head = `<tr>${splitRow(header)
120
- .map((c, i) => cell(c, i, 'th'))
121
- .join('')}</tr>`;
122
- const rows = body
123
- .split('\n')
124
- .filter((r) => r.trim())
125
- .map((r) => `<tr>${splitRow(r)
126
- .map((c, i) => cell(c, i, 'td'))
127
- .join('')}</tr>`)
128
- .join('');
128
+ const head = row(header, 'th');
129
+ const rows = body.map((r) => row(r, 'td')).join('');
129
130
  return `<table><thead>${head}</thead><tbody>${rows}</tbody></table>`;
130
131
  }
132
+ // GFM pipe tables on escaped text: a header row, a delimiter row with the
133
+ // same cell count, then rows up to a blank line or the start of another block.
134
+ function tables(s, saved, autolink) {
135
+ const lines = s.split('\n');
136
+ const out = [];
137
+ for (let i = 0; i < lines.length; i++) {
138
+ const head = lines[i];
139
+ const next = lines[i + 1];
140
+ if (next === undefined || !hasPipe(head) || !hasPipe(next) || TABLE_BREAK.test(head)) {
141
+ out.push(head);
142
+ continue;
143
+ }
144
+ const delim = splitRow(next);
145
+ const cols = splitRow(head);
146
+ if (!delim.every((c) => DELIM_CELL.test(c)) || cols.length !== delim.length) {
147
+ out.push(head);
148
+ continue;
149
+ }
150
+ let j = i + 2;
151
+ while (j < lines.length && !TABLE_BREAK.test(lines[j]))
152
+ j++;
153
+ out.push('', stash(saved, table(head, delim, lines.slice(i + 2, j), autolink)), '');
154
+ i = j - 1;
155
+ }
156
+ return out.join('\n');
157
+ }
131
158
  // Block pass over the escaped, inline-rendered lines. Produces paragraphs,
132
159
  // headings, lists, blockquotes and rules; stashed blocks stay opaque.
133
160
  function blocks(lines) {
@@ -209,7 +236,7 @@ function blocks(lines) {
209
236
  return out.join('');
210
237
  }
211
238
  export function renderMarkdown(src, opts = {}) {
212
- const tables = opts.tables !== false;
239
+ const withTables = opts.tables !== false;
213
240
  const autolink = opts.autolink !== false;
214
241
  const saved = [];
215
242
  let s = stripAnsi(src.replace(/\r\n?/g, '\n'));
@@ -221,9 +248,8 @@ export function renderMarkdown(src, opts = {}) {
221
248
  return stash(saved, `<pre${attr}><code>${body}</code></pre>`);
222
249
  });
223
250
  s = escapeHtml(s);
224
- if (tables) {
225
- s = s.replace(TABLE_RE, (_m, header, delim, body) => `\n${stash(saved, table(header, delim, body, autolink))}\n`);
226
- }
251
+ if (withTables)
252
+ s = tables(s, saved, autolink);
227
253
  s = inline(s, autolink);
228
254
  s = blocks(s.split('\n'));
229
255
  return restore(s, saved);
@@ -0,0 +1,3 @@
1
+ /** Sets `--pointer-x` / `--pointer-y` (px, box-relative) on the hovered
2
+ * `[data-tsu]` element and its `[data-tsu]` ancestors, mouse only. */
3
+ export declare function trackPointer(doc?: Document | undefined): () => void;
@@ -0,0 +1,40 @@
1
+ /** Sets `--pointer-x` / `--pointer-y` (px, box-relative) on the hovered
2
+ * `[data-tsu]` element and its `[data-tsu]` ancestors, mouse only. */
3
+ export function trackPointer(doc = globalThis.document) {
4
+ if (!doc)
5
+ return () => { };
6
+ const win = doc.defaultView;
7
+ let last = null;
8
+ let frame = 0;
9
+ const flush = () => {
10
+ frame = 0;
11
+ const e = last;
12
+ last = null;
13
+ if (!e)
14
+ return;
15
+ let el = e.target?.closest?.('[data-tsu]');
16
+ while (el) {
17
+ const r = el.getBoundingClientRect();
18
+ el.style.setProperty('--pointer-x', `${e.clientX - r.left}px`);
19
+ el.style.setProperty('--pointer-y', `${e.clientY - r.top}px`);
20
+ el = el.parentElement?.closest('[data-tsu]');
21
+ }
22
+ };
23
+ const move = (e) => {
24
+ if (e.pointerType !== 'mouse')
25
+ return;
26
+ last = e;
27
+ if (!win)
28
+ flush();
29
+ else if (!frame)
30
+ frame = win.requestAnimationFrame(flush);
31
+ };
32
+ doc.addEventListener('pointermove', move, { passive: true });
33
+ return () => {
34
+ doc.removeEventListener('pointermove', move);
35
+ if (frame)
36
+ win?.cancelAnimationFrame(frame);
37
+ frame = 0;
38
+ last = null;
39
+ };
40
+ }
@@ -0,0 +1,17 @@
1
+ import { type FxFamily, type FxSettings, type FxVariant } from '../fx';
2
+ export declare const FX_PREPAINT: string;
3
+ declare class Fx {
4
+ settings: FxSettings;
5
+ onchange?: (settings: FxSettings) => void;
6
+ constructor();
7
+ get<F extends FxFamily>(family: F): FxVariant<F>;
8
+ set<F extends FxFamily>(family: F, variant: string): void;
9
+ reset(): void;
10
+ get isDefault(): boolean;
11
+ /** Replay settings an app persisted itself, without echoing through `onchange`. */
12
+ hydrate(settings: Partial<FxSettings>): void;
13
+ private commit;
14
+ private apply;
15
+ }
16
+ export declare const fx: Fx;
17
+ export {};
@@ -0,0 +1,64 @@
1
+ import { browser } from '../env';
2
+ import { DEFAULT_FX, FX_FAMILIES, fxAttr, fxDiff, fxPrepaint, isFxVariant, parseFx, } from '../fx';
3
+ import { readStorage, writeStorage } from './storage';
4
+ // Until something is saved or set, <html> is left untouched so attributes an
5
+ // app wrote itself win.
6
+ const KEY = 'tsumikit-fx';
7
+ export const FX_PREPAINT = fxPrepaint(KEY);
8
+ class Fx {
9
+ settings = $state({ ...DEFAULT_FX });
10
+ onchange;
11
+ constructor() {
12
+ if (!browser)
13
+ return;
14
+ const raw = readStorage(KEY);
15
+ if (!raw)
16
+ return;
17
+ try {
18
+ this.settings = parseFx(JSON.parse(raw));
19
+ }
20
+ catch { }
21
+ this.apply();
22
+ }
23
+ get(family) {
24
+ return this.settings[family];
25
+ }
26
+ set(family, variant) {
27
+ if (!isFxVariant(family, variant))
28
+ return;
29
+ this.settings = { ...this.settings, [family]: variant };
30
+ this.commit();
31
+ }
32
+ reset() {
33
+ this.settings = { ...DEFAULT_FX };
34
+ this.commit();
35
+ }
36
+ get isDefault() {
37
+ return Object.keys(fxDiff(this.settings)).length === 0;
38
+ }
39
+ /** Replay settings an app persisted itself, without echoing through `onchange`. */
40
+ hydrate(settings) {
41
+ this.settings = parseFx(settings);
42
+ this.apply();
43
+ if (browser)
44
+ writeStorage(KEY, JSON.stringify(fxDiff(this.settings)));
45
+ }
46
+ commit() {
47
+ this.apply();
48
+ if (browser)
49
+ writeStorage(KEY, JSON.stringify(fxDiff(this.settings)));
50
+ this.onchange?.(this.settings);
51
+ }
52
+ apply() {
53
+ if (!browser)
54
+ return;
55
+ const root = document.documentElement;
56
+ for (const f of FX_FAMILIES) {
57
+ if (this.settings[f] === DEFAULT_FX[f])
58
+ root.removeAttribute(fxAttr(f));
59
+ else
60
+ root.setAttribute(fxAttr(f), this.settings[f]);
61
+ }
62
+ }
63
+ }
64
+ export const fx = new Fx();
@@ -1,4 +1,4 @@
1
- import { type SlotOf, type ThemeChoice, type ThemePreference } from '../theme-mode';
1
+ import { type LookId, type SlotOf, type ThemeChoice, type ThemePreference } from '../theme-mode';
2
2
  export declare const THEMES: readonly [{
3
3
  readonly id: "light";
4
4
  readonly label: "Light";
@@ -186,6 +186,8 @@ export declare const THEMES: readonly [{
186
186
  readonly themeColor: "#3f3f3f";
187
187
  readonly mode: "dark";
188
188
  }];
189
+ /** Pre-paint script for the built-in themes; inline it in <head>. */
190
+ export declare const THEME_PREPAINT: string;
189
191
  export type Mode = (typeof THEMES)[number]['id'];
190
192
  export type ThemeMode = (typeof THEMES)[number]['mode'];
191
193
  export type ThemeId = Mode | (string & {});
@@ -222,6 +224,8 @@ declare class Theme {
222
224
  setDefault(id: ThemeId): void;
223
225
  private resolve;
224
226
  private read;
227
+ /** The active structural look; `default` when none is set. */
228
+ get look(): LookId;
225
229
  private paint;
226
230
  private persist;
227
231
  private apply;
@@ -234,8 +238,10 @@ declare class Theme {
234
238
  /** A picker choice: `auto`, or a theme id, which also becomes its slot's
235
239
  * remembered theme. An unregistered id is ignored. */
236
240
  choose(choice: string): ThemePreference;
241
+ /** Pick a look; unknown ids fall back to the default look. */
242
+ setLook(look: string): ThemePreference;
237
243
  /** Replay a preference an app persisted itself, without echoing it back
238
- * through `onchange`. */
244
+ * through `onchange`. A preference without `look` keeps the current one. */
239
245
  hydrate(pref: ThemePreference): void;
240
246
  }
241
247
  export declare const theme: Theme;
@@ -1,5 +1,5 @@
1
1
  import { browser } from '../env';
2
- import { AUTO_THEME, chooseTheme, DEFAULT_THEME_PREFERENCE, preferenceFrom, resolveTheme, } from '../theme-mode';
2
+ import { AUTO_THEME, chooseTheme, DEFAULT_LOOK, DEFAULT_THEME_PREFERENCE, parseLook, preferenceFrom, resolveTheme, themePrepaint, } from '../theme-mode';
3
3
  import { readStorage, writeStorage } from './storage';
4
4
  // Theme registry. Built-ins live in THEMES (+ one [data-theme="id"] block in
5
5
  // styles/themes.css); consumers append their own with theme.register() and ship
@@ -8,7 +8,8 @@ import { readStorage, writeStorage } from './storage';
8
8
  // light/dark sections.
9
9
  //
10
10
  // `tsumikit-theme` holds a {mode,light,dark} blob; a bare theme id there is a
11
- // legacy value and migrates through `preferenceFrom`.
11
+ // legacy value and migrates through `preferenceFrom`. The blob also carries
12
+ // `look` when it is not the default.
12
13
  const KEY = 'tsumikit-theme';
13
14
  const SCHEME_QUERY = '(prefers-color-scheme: dark)';
14
15
  export const THEMES = [
@@ -67,6 +68,8 @@ export const THEMES = [
67
68
  { id: 'espresso', label: 'Espresso', icon: '◉', themeColor: '#463a31', mode: 'dark' },
68
69
  { id: 'zenburn', label: 'Zenburn', icon: 'Z', themeColor: '#3f3f3f', mode: 'dark' },
69
70
  ];
71
+ /** Pre-paint script for the built-in themes; inline it in <head>. */
72
+ export const THEME_PREPAINT = themePrepaint(KEY, THEMES);
70
73
  const FALLBACK_ICON = '◈';
71
74
  class Theme {
72
75
  current = $state('dark');
@@ -142,9 +145,14 @@ class Theme {
142
145
  blob = parsed;
143
146
  }
144
147
  catch { }
145
- return preferenceFrom(blob
148
+ const pref = preferenceFrom(blob
146
149
  ? { themeMode: blob.mode, lightTheme: blob.light, darkTheme: blob.dark }
147
150
  : { theme: this.saved }, this.slotOf, seed);
151
+ return withLook(pref, blob?.look);
152
+ }
153
+ /** The active structural look; `default` when none is set. */
154
+ get look() {
155
+ return parseLook(this.pref.look);
148
156
  }
149
157
  paint() {
150
158
  this.current = this.resolved;
@@ -159,7 +167,13 @@ class Theme {
159
167
  apply() {
160
168
  if (!browser)
161
169
  return;
162
- document.documentElement.setAttribute('data-theme', this.current);
170
+ const root = document.documentElement;
171
+ root.setAttribute('data-theme', this.current);
172
+ root.setAttribute('data-mode', this.option.mode);
173
+ if (this.look === DEFAULT_LOOK)
174
+ root.removeAttribute('data-look');
175
+ else
176
+ root.setAttribute('data-look', this.look);
163
177
  const color = this.option.themeColor;
164
178
  if (color)
165
179
  document
@@ -194,12 +208,24 @@ class Theme {
194
208
  this.persist();
195
209
  return this.pref;
196
210
  }
211
+ /** Pick a look; unknown ids fall back to the default look. */
212
+ setLook(look) {
213
+ this.pref = withLook(this.pref, look);
214
+ this.apply();
215
+ this.persist();
216
+ return this.pref;
217
+ }
197
218
  /** Replay a preference an app persisted itself, without echoing it back
198
- * through `onchange`. */
219
+ * through `onchange`. A preference without `look` keeps the current one. */
199
220
  hydrate(pref) {
200
- this.pref = pref;
201
- this.saved = JSON.stringify(pref);
221
+ this.pref = withLook(pref, 'look' in pref ? pref.look : this.pref.look);
222
+ this.saved = JSON.stringify(this.pref);
202
223
  this.paint();
203
224
  }
204
225
  }
226
+ function withLook(pref, look) {
227
+ const { look: _, ...rest } = pref;
228
+ const id = parseLook(look);
229
+ return id === DEFAULT_LOOK ? rest : { ...rest, look: id };
230
+ }
205
231
  export const theme = new Theme();