anentrypoint-design 0.0.251 → 0.0.253

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": "anentrypoint-design",
3
- "version": "0.0.251",
3
+ "version": "0.0.253",
4
4
  "description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
5
5
  "type": "module",
6
6
  "main": "./dist/247420.js",
@@ -615,18 +615,123 @@ export function Pager({ page = 1, pageCount = 1, onPage, total, itemLabel = 'ite
615
615
  }
616
616
 
617
617
  // ---------------------------------------------------------------------------
618
- // JsonViewer — pre-formatted monospace data preview (max-height + scroll),
619
- // generalizing gmsniff's gm-json. Accepts a pre-stringified string OR any
620
- // value (objects/arrays get JSON.stringify(v, null, 2); null/undefined render
621
- // the empty-state text rather than the literal string "undefined"/"null").
618
+ // JsonViewer — monospace data preview (max-height + scroll), generalizing
619
+ // gmsniff's gm-json. Accepts a pre-stringified string OR any value
620
+ // (objects/arrays get JSON.stringify(v, null, 2); null/undefined render the
621
+ // empty-state text rather than the literal string "undefined"/"null").
622
+ //
623
+ // mode selects rendering; 'plain' is the historical contract (children[0] is
624
+ // the raw text string, verbatim for string input) and stays the default so
625
+ // every existing consumer is untouched:
626
+ // 'plain' — flat <pre>, raw text.
627
+ // 'highlight' — flat <pre>, text tokenized into ds-ep-json-* spans
628
+ // (key/string/number/boolean/null). A string that does not
629
+ // parse as JSON falls back to plain text — arbitrary prose is
630
+ // never falsely tokenized.
631
+ // 'tree' — collapsible <details> tree per nested object/array, open
632
+ // above treeDepth (default 2), each summary carrying a
633
+ // child-count tag. Scalars/unparseable input fall back to
634
+ // 'highlight'/plain respectively.
635
+ // copyable=true wraps the viewer with a copy-to-clipboard button (transient
636
+ // copied/failed feedback, no dependencies).
622
637
  // ---------------------------------------------------------------------------
623
- export function JsonViewer({ value, emptyText = 'no data', maxHeight } = {}) {
624
- let text;
638
+ const JSON_NUM_CHARS = '0123456789eE+.-';
639
+
640
+ // Linear single-pass scan — no regex, no backtracking, safe on truncated
641
+ // input (an unterminated string just consumes to end-of-text).
642
+ function tokenizeJson(text) {
643
+ const toks = [];
644
+ let i = 0, plain = '';
645
+ const flush = () => { if (plain) { toks.push(['', plain]); plain = ''; } };
646
+ while (i < text.length) {
647
+ const c = text[i];
648
+ if (c === '"') {
649
+ const start = i;
650
+ i++;
651
+ while (i < text.length && text[i] !== '"') { if (text[i] === '\\') i++; i++; }
652
+ i = Math.min(i + 1, text.length);
653
+ let j = i;
654
+ while (j < text.length && (text[j] === ' ' || text[j] === '\t' || text[j] === '\n' || text[j] === '\r')) j++;
655
+ flush();
656
+ toks.push([text[j] === ':' ? 'k' : 's', text.slice(start, i)]);
657
+ continue;
658
+ }
659
+ if (c === '-' || (c >= '0' && c <= '9')) {
660
+ const start = i;
661
+ i++;
662
+ while (i < text.length && JSON_NUM_CHARS.includes(text[i])) i++;
663
+ flush();
664
+ toks.push(['n', text.slice(start, i)]);
665
+ continue;
666
+ }
667
+ if (text.startsWith('true', i)) { flush(); toks.push(['b', 'true']); i += 4; continue; }
668
+ if (text.startsWith('false', i)) { flush(); toks.push(['b', 'false']); i += 5; continue; }
669
+ if (text.startsWith('null', i)) { flush(); toks.push(['z', 'null']); i += 4; continue; }
670
+ plain += c; i++;
671
+ }
672
+ flush();
673
+ return toks;
674
+ }
675
+
676
+ function highlightJsonSpans(text) {
677
+ return tokenizeJson(text).map(([t, s]) => t ? h('span', { class: 'ds-ep-json-' + t }, s) : s);
678
+ }
679
+
680
+ function jsonTreeNode(key, val, depth, treeDepth) {
681
+ const keyParts = key != null ? [h('span', { class: 'ds-ep-json-k' }, JSON.stringify(key)), ': '] : [];
682
+ if (val !== null && typeof val === 'object') {
683
+ const isArr = Array.isArray(val);
684
+ const entries = isArr ? val.map((v) => [null, v]) : Object.entries(val);
685
+ if (!entries.length) return h('div', { class: 'ds-ep-json-leaf' }, ...keyParts, isArr ? '[]' : '{}');
686
+ const tag = isArr ? '[' + entries.length + ']' : '{' + entries.length + '}';
687
+ return h('details', { class: 'ds-ep-json-node', open: depth < treeDepth ? true : null },
688
+ h('summary', { class: 'ds-ep-json-sum' }, ...keyParts, h('span', { class: 'ds-ep-json-tag' }, tag)),
689
+ h('div', { class: 'ds-ep-json-kids' }, ...entries.map(([k, v]) => jsonTreeNode(k, v, depth + 1, treeDepth))));
690
+ }
691
+ const t = typeof val === 'string' ? 's' : typeof val === 'number' ? 'n' : typeof val === 'boolean' ? 'b' : 'z';
692
+ return h('div', { class: 'ds-ep-json-leaf' }, ...keyParts, h('span', { class: 'ds-ep-json-' + t }, JSON.stringify(val) ?? String(val)));
693
+ }
694
+
695
+ function jsonCopyButton(text) {
696
+ return h('button', {
697
+ type: 'button', class: 'ds-ep-json-copy', title: 'copy JSON', 'aria-label': 'copy JSON',
698
+ onclick: (e) => {
699
+ const btn = e.currentTarget;
700
+ const show = (label, ok) => {
701
+ btn.textContent = label;
702
+ btn.classList.toggle('copied', ok);
703
+ setTimeout(() => { btn.textContent = 'copy'; btn.classList.remove('copied'); }, 1200);
704
+ };
705
+ try {
706
+ navigator.clipboard.writeText(text).then(() => show('copied', true), () => show('failed', false));
707
+ } catch {
708
+ show('failed', false);
709
+ }
710
+ },
711
+ }, 'copy');
712
+ }
713
+
714
+ export function JsonViewer({ value, emptyText = 'no data', maxHeight, mode = 'plain', copyable = false, treeDepth = 2 } = {}) {
715
+ let text, parsed;
716
+ let knownJson = false;
625
717
  if (value == null) text = null;
626
718
  else if (typeof value === 'string') text = value;
627
- else { try { text = JSON.stringify(value, null, 2); } catch { text = String(value); } }
719
+ else { try { text = JSON.stringify(value, null, 2); knownJson = text != null; parsed = value; } catch { text = String(value); } }
628
720
  if (!text) return h('div', { class: 'ds-ep-json ds-ep-json-empty' }, emptyText);
629
- return h('pre', { class: 'ds-ep-json', style: maxHeight ? ('max-height:' + maxHeight) : null }, text);
721
+ const style = maxHeight ? ('max-height:' + maxHeight) : null;
722
+ if (!knownJson && (mode === 'highlight' || mode === 'tree')) {
723
+ try { parsed = JSON.parse(text); knownJson = true; } catch { /* not JSON — render plain */ }
724
+ }
725
+ let body;
726
+ if (mode === 'tree' && knownJson && parsed !== null && typeof parsed === 'object') {
727
+ body = h('div', { class: 'ds-ep-json ds-ep-json-tree', style }, jsonTreeNode(null, parsed, 0, treeDepth));
728
+ } else if ((mode === 'highlight' || mode === 'tree') && knownJson) {
729
+ body = h('pre', { class: 'ds-ep-json ds-ep-json-hl', style }, ...highlightJsonSpans(text));
730
+ } else {
731
+ body = h('pre', { class: 'ds-ep-json', style }, text);
732
+ }
733
+ if (!copyable) return body;
734
+ return h('div', { class: 'ds-ep-json-wrap' }, jsonCopyButton(text), body);
630
735
  }
631
736
 
632
737
  export function IconButtonGroup({ items = [], value, onChange, dense = false } = {}) {