@leftium/gg 0.0.29 → 0.0.31

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.
@@ -95,6 +95,20 @@ export function createGgPlugin(options, gg) {
95
95
  }
96
96
  });
97
97
  }
98
+ function soloNamespace(namespace) {
99
+ const ns = namespace.trim();
100
+ // Toggle: if already soloed on this namespace, restore all
101
+ if (filterPattern === ns) {
102
+ filterPattern = 'gg:*';
103
+ enabledNamespaces.clear();
104
+ getAllCapturedNamespaces().forEach((n) => enabledNamespaces.add(n));
105
+ return;
106
+ }
107
+ // Solo: show only this namespace
108
+ filterPattern = ns;
109
+ enabledNamespaces.clear();
110
+ enabledNamespaces.add(ns);
111
+ }
98
112
  function simplifyPattern(pattern) {
99
113
  if (!pattern)
100
114
  return '';
@@ -172,7 +186,11 @@ export function createGgPlugin(options, gg) {
172
186
  }
173
187
  function gridColumns() {
174
188
  const ns = nsColWidth !== null ? `${nsColWidth}px` : 'auto';
175
- // diff | ns | handle | content (× is now inside ns)
189
+ // When filter expanded: icons | diff | ns | handle | content
190
+ // When collapsed: diff | ns | handle | content
191
+ if (filterExpanded) {
192
+ return `auto auto ${ns} 4px 1fr`;
193
+ }
176
194
  return `auto ${ns} 4px 1fr`;
177
195
  }
178
196
  function buildHTML() {
@@ -191,6 +209,7 @@ export function createGgPlugin(options, gg) {
191
209
  .gg-log-header {
192
210
  display: contents;
193
211
  }
212
+ .gg-log-icons,
194
213
  .gg-log-diff,
195
214
  .gg-log-ns,
196
215
  .gg-log-handle,
@@ -199,6 +218,32 @@ export function createGgPlugin(options, gg) {
199
218
  align-self: start !important;
200
219
  border-top: 1px solid rgba(0,0,0,0.05);
201
220
  }
221
+ .gg-log-icons {
222
+ display: flex;
223
+ gap: 2px;
224
+ padding: 0 4px 0 0;
225
+ white-space: nowrap;
226
+ align-self: stretch !important;
227
+ }
228
+ .gg-log-icons button {
229
+ all: unset;
230
+ cursor: pointer;
231
+ opacity: 0.35;
232
+ padding: 4px 10px;
233
+ line-height: 1;
234
+ display: flex;
235
+ align-items: center;
236
+ }
237
+ .gg-log-icons button:hover {
238
+ opacity: 1;
239
+ background: rgba(0,0,0,0.05);
240
+ }
241
+ .gg-solo-target {
242
+ cursor: pointer;
243
+ }
244
+ .gg-solo-target:hover {
245
+ background: rgba(0,0,0,0.05);
246
+ }
202
247
  .gg-details {
203
248
  grid-column: 1 / -1;
204
249
  border-top: none;
@@ -242,15 +287,6 @@ export function createGgPlugin(options, gg) {
242
287
  word-break: break-word;
243
288
  padding: 4px 0;
244
289
  }
245
- /* Make header clickable for filtering when filters are expanded */
246
- .gg-log-header.clickable {
247
- cursor: pointer;
248
- }
249
- /* Desktop: highlight child elements since header has display: contents */
250
- .gg-log-header.clickable:hover .gg-log-diff,
251
- .gg-log-header.clickable:hover .gg-log-ns {
252
- background: rgba(0,0,0,0.05);
253
- }
254
290
  .gg-filter-panel {
255
291
  background: #f5f5f5;
256
292
  padding: 10px;
@@ -333,6 +369,7 @@ export function createGgPlugin(options, gg) {
333
369
  .gg-log-entry:not(:first-child) {
334
370
  border-top: 1px solid rgba(0,0,0,0.05);
335
371
  }
372
+ .gg-log-icons,
336
373
  .gg-log-diff,
337
374
  .gg-log-ns,
338
375
  .gg-log-handle,
@@ -347,18 +384,6 @@ export function createGgPlugin(options, gg) {
347
384
  margin-bottom: 4px;
348
385
  min-width: 0;
349
386
  }
350
- /* Mobile: hover on container since it's not display: contents */
351
- .gg-log-header.clickable {
352
- padding: 2px 0;
353
- }
354
- .gg-log-header.clickable:hover {
355
- background: rgba(0,0,0,0.05);
356
- }
357
- /* Override desktop child hover on mobile */
358
- .gg-log-header.clickable:hover .gg-log-diff,
359
- .gg-log-header.clickable:hover .gg-log-ns {
360
- background: transparent;
361
- }
362
387
  .gg-log-diff {
363
388
  padding: 0;
364
389
  text-align: left;
@@ -612,17 +637,42 @@ export function createGgPlugin(options, gg) {
612
637
  }
613
638
  return;
614
639
  }
615
- // Handle clickable header (when filters expanded)
616
- // Skip if clicking on resize handle
617
- if (!target?.classList?.contains('gg-log-handle') &&
618
- target?.closest('.gg-log-header.clickable')) {
619
- const header = target.closest('.gg-log-header.clickable');
620
- const namespace = header.getAttribute('data-namespace');
640
+ // Handle filter icon clicks (hide / solo)
641
+ if (target?.classList?.contains('gg-icon-hide') ||
642
+ target?.classList?.contains('gg-icon-solo')) {
643
+ const iconsDiv = target.closest('.gg-log-icons');
644
+ const namespace = iconsDiv?.getAttribute('data-namespace');
645
+ if (!namespace)
646
+ return;
647
+ if (target.classList.contains('gg-icon-hide')) {
648
+ toggleNamespace(namespace, false);
649
+ }
650
+ else {
651
+ soloNamespace(namespace);
652
+ }
653
+ localStorage.setItem('debug', filterPattern);
654
+ renderFilterUI();
655
+ renderLogs();
656
+ return;
657
+ }
658
+ // Handle clicking diff/ns cells to solo (same as 🎯)
659
+ if (target?.classList?.contains('gg-solo-target')) {
660
+ const namespace = target.getAttribute('data-namespace');
621
661
  if (!namespace)
622
662
  return;
623
- // Toggle this namespace off
624
- toggleNamespace(namespace, false);
625
- // Save to localStorage and re-render
663
+ soloNamespace(namespace);
664
+ localStorage.setItem('debug', filterPattern);
665
+ renderFilterUI();
666
+ renderLogs();
667
+ return;
668
+ }
669
+ // Clicking background (container or grid, not a log element) restores all
670
+ if (filterExpanded &&
671
+ filterPattern !== 'gg:*' &&
672
+ (target === containerEl || target?.classList?.contains('gg-log-grid'))) {
673
+ filterPattern = 'gg:*';
674
+ enabledNamespaces.clear();
675
+ getAllCapturedNamespaces().forEach((ns) => enabledNamespaces.add(ns));
626
676
  localStorage.setItem('debug', filterPattern);
627
677
  renderFilterUI();
628
678
  renderLogs();
@@ -727,20 +777,22 @@ export function createGgPlugin(options, gg) {
727
777
  })
728
778
  .join(' ');
729
779
  }
730
- // Make header clickable when filters expanded
731
- const headerClass = filterExpanded ? 'gg-log-header clickable' : 'gg-log-header';
732
- const headerAttrs = filterExpanded
733
- ? ` data-namespace="${ns}" title="Click to hide this namespace"`
734
- : '';
735
- // Add × at start of diff when filters expanded (bold, darker)
736
- const filterIcon = filterExpanded
737
- ? '<span style="font-weight: bold; color: #000; opacity: 0.6;">× </span>'
780
+ // Filter icons column (only when expanded)
781
+ const iconsCol = filterExpanded
782
+ ? `<div class="gg-log-icons" data-namespace="${ns}">` +
783
+ `<button class="gg-icon-hide" title="Hide this namespace">🗑</button>` +
784
+ `<button class="gg-icon-solo" title="Show only this namespace">🎯</button>` +
785
+ `</div>`
738
786
  : '';
787
+ // When filter expanded, diff+ns are clickable (solo) with data-namespace
788
+ const soloAttr = filterExpanded ? ` data-namespace="${ns}"` : '';
789
+ const soloClass = filterExpanded ? ' gg-solo-target' : '';
739
790
  // Desktop: grid layout, Mobile: stacked layout
740
791
  return (`<div class="gg-log-entry">` +
741
- `<div class="${headerClass}"${headerAttrs}>` +
742
- `<div class="gg-log-diff" style="color: ${color};">${filterIcon}${diff}</div>` +
743
- `<div class="gg-log-ns" style="color: ${color};">${ns}</div>` +
792
+ `<div class="gg-log-header">` +
793
+ iconsCol +
794
+ `<div class="gg-log-diff${soloClass}" style="color: ${color};"${soloAttr}>${diff}</div>` +
795
+ `<div class="gg-log-ns${soloClass}" style="color: ${color};"${soloAttr}>${ns}</div>` +
744
796
  `<div class="gg-log-handle"></div>` +
745
797
  `</div>` +
746
798
  `<div class="gg-log-content">${argsHTML}</div>` +
package/dist/gg.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import debugFactory from './debug.js';
2
2
  import ErrorStackParser from 'error-stack-parser';
3
3
  import { BROWSER, DEV } from 'esm-env';
4
+ import { toWordTuple } from './words.js';
4
5
  const _ggCallSitesPlugin = typeof __GG_TAG_PLUGIN__ !== 'undefined' ? __GG_TAG_PLUGIN__ : false;
5
6
  /**
6
7
  * Creates a debug instance with custom formatArgs to add namespace padding
@@ -166,6 +167,8 @@ const srcRootRegex = new RegExp(ggConfig.srcRootPattern, 'i');
166
167
  // - Cache and reuse the same log function for a given callpoint.
167
168
  const namespaceToLogFunction = new Map();
168
169
  let maxCallpointLength = 0;
170
+ // Cache: raw stack line → word tuple (avoids re-hashing the same call site)
171
+ const stackLineCache = new Map();
169
172
  /**
170
173
  * Reset the namespace width tracking.
171
174
  * Useful after configuration checks that may have long callpoint paths.
@@ -189,27 +192,50 @@ export function gg(...args) {
189
192
  // When ggCallSitesPlugin is installed, all bare gg() calls are rewritten to gg.ns()
190
193
  // at build time, so this code path only runs for un-transformed calls.
191
194
  // Skip expensive stack parsing if the plugin is handling callpoints.
192
- // In development without plugin: calculate detailed callpoint information
193
- // In production without plugin: skip expensive stack parsing and use simple namespace
194
- if (DEV && !_ggCallSitesPlugin) {
195
- // Ignore first stack frame, which is always the call to gg() itself.
196
- stack = ErrorStackParser.parse(new Error()).splice(1);
197
- // Example: http://localhost:5173/src/routes/+page.svelte
198
- const filename = stack[0].fileName?.replace(timestampRegex, '') || '';
199
- // Example: src/routes/+page.svelte
200
- const filenameToOpen = filename.replace(srcRootRegex, '$<folderName>/');
201
- url = openInEditorUrl(filenameToOpen);
202
- // Example: routes/+page.svelte
203
- fileName = filename.replace(srcRootRegex, '');
204
- functionName = stack[0].functionName || '';
205
- // A callpoint is uniquely identified by the filename plus function name
206
- const callpoint = `${fileName}${functionName ? `@${functionName}` : ''}`;
207
- if (callpoint.length < 80 && callpoint.length > maxCallpointLength) {
208
- maxCallpointLength = callpoint.length;
195
+ if (!_ggCallSitesPlugin) {
196
+ if (DEV) {
197
+ // Development without plugin: full stack parsing for detailed callpoint info
198
+ // Ignore first stack frame, which is always the call to gg() itself.
199
+ stack = ErrorStackParser.parse(new Error()).splice(1);
200
+ // Example: http://localhost:5173/src/routes/+page.svelte
201
+ const filename = stack[0].fileName?.replace(timestampRegex, '') || '';
202
+ // Example: src/routes/+page.svelte
203
+ const filenameToOpen = filename.replace(srcRootRegex, '$<folderName>/');
204
+ url = openInEditorUrl(filenameToOpen);
205
+ // Example: routes/+page.svelte
206
+ fileName = filename.replace(srcRootRegex, '');
207
+ functionName = stack[0].functionName || '';
208
+ // A callpoint is uniquely identified by the filename plus function name
209
+ const callpoint = `${fileName}${functionName ? `@${functionName}` : ''}`;
210
+ if (callpoint.length < 80 && callpoint.length > maxCallpointLength) {
211
+ maxCallpointLength = callpoint.length;
212
+ }
213
+ // Namespace without padding - keeps colors stable
214
+ // Editor link appended if enabled
215
+ namespace = `gg:${callpoint}${ggConfig.editorLink ? url : ''}`;
216
+ }
217
+ else {
218
+ // Production without plugin: cheap stack hash → deterministic word tuple
219
+ // Avoids expensive ErrorStackParser; just grabs the raw stack line and hashes it.
220
+ // Same call site always produces the same word pair (e.g. "calm-fox").
221
+ const rawStack = new Error().stack || '';
222
+ // Stack line [2]: skip "Error" header [0] and gg() frame [1]
223
+ const callerLine = rawStack.split('\n')[2] || rawStack;
224
+ // Strip line:col numbers so all gg() calls within the same function
225
+ // hash to the same word tuple. In minified builds, multiple gg() calls
226
+ // in one function differ only by column offset — we want them grouped.
227
+ // Chrome: "at handleClick (chunk-abc.js:1:45892)" → "at handleClick (chunk-abc.js)"
228
+ // Firefox: "handleClick@https://...:1:45892" → "handleClick@https://..."
229
+ const callerKey = callerLine.replace(/:\d+:\d+\)?$/, '').trim();
230
+ const callpoint = stackLineCache.get(callerKey) ?? toWordTuple(callerKey);
231
+ if (!stackLineCache.has(callerKey)) {
232
+ stackLineCache.set(callerKey, callpoint);
233
+ }
234
+ if (callpoint.length < 80 && callpoint.length > maxCallpointLength) {
235
+ maxCallpointLength = callpoint.length;
236
+ }
237
+ namespace = `gg:${callpoint}`;
209
238
  }
210
- // Namespace without padding - keeps colors stable
211
- // Editor link appended if enabled
212
- namespace = `gg:${callpoint}${ggConfig.editorLink ? url : ''}`;
213
239
  }
214
240
  const ggLogFunction = namespaceToLogFunction.get(namespace) ||
215
241
  namespaceToLogFunction.set(namespace, createGgDebugger(namespace)).get(namespace);
@@ -527,7 +553,7 @@ export async function runGgDiagnostics() {
527
553
  message(`${checkbox(ggLogTest.enabled)} DEBUG env variable: ${process?.env?.DEBUG}${hint}`);
528
554
  }
529
555
  // Optional plugin diagnostics listed last
530
- message(makeHint(_ggCallSitesPlugin, `✅ (optional) gg-call-sites vite plugin detected! Call-site namespaces baked in at build time.`, `⚠️ (optional) gg-call-sites vite plugin not detected. Add ggCallSitesPlugin() to vite.config.ts for build-time call-site namespaces (needed for useful namespaces in prod, faster/more reliable in dev)`));
556
+ message(makeHint(_ggCallSitesPlugin, `✅ (optional) gg-call-sites vite plugin detected! Call-site namespaces baked in at build time.`, `⚠️ (optional) gg-call-sites vite plugin not detected. Add ggCallSitesPlugin() to vite.config.ts for build-time call-site namespaces (faster/more reliable). Without plugin, prod uses word-tuple names (e.g. calm-fox) as unique call-site identifiers.`));
531
557
  if (BROWSER && DEV) {
532
558
  const { status } = await fetch('/__open-in-editor?file=+');
533
559
  message(makeHint(status === 222, `✅ (optional) open-in-editor vite plugin detected! (status code: ${status}) Clickable links open source files in editor.`, `⚠️ (optional) open-in-editor vite plugin not detected. (status code: ${status}) Add openInEditorPlugin() to vite.config.ts for clickable links that open source files in editor`));
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Word lists for generating deterministic human-readable callpoint names.
3
+ * Used as a fallback in production when the gg-call-sites Vite plugin is not installed.
4
+ *
5
+ * Each list has exactly 256 entries (8-bit index) so a 32-bit hash cleanly maps to
6
+ * an adjective-noun pair: adjectives[hash & 0xFF] + "-" + nouns[(hash >> 8) & 0xFF]
7
+ * giving 65,536 unique combinations.
8
+ *
9
+ * Selection criteria:
10
+ * - Short (3-6 chars) for compact console output
11
+ * - Visually distinct (no near-homoglyphs like fast/last)
12
+ * - Inoffensive
13
+ */
14
+ export declare const adjectives: readonly ["able", "acid", "aged", "airy", "apt", "avid", "awry", "balmy", "bare", "beefy", "bent", "big", "bland", "bleak", "blind", "bliss", "blue", "blunt", "bold", "brash", "brave", "brief", "briny", "brisk", "broad", "buff", "bulky", "bumpy", "burly", "busy", "calm", "cheap", "chewy", "chief", "chilly", "civil", "clean", "clear", "close", "cold", "cool", "coral", "cozy", "crisp", "cubic", "curly", "curvy", "cute", "cyan", "damp", "dark", "dear", "deep", "dense", "dewy", "dim", "dizzy", "dopey", "dorky", "draft", "dry", "dual", "dull", "dusty", "eager", "early", "easy", "edgy", "elfin", "elite", "empty", "equal", "even", "every", "evil", "exact", "extra", "faded", "fair", "fancy", "fast", "few", "fine", "firm", "first", "fishy", "fit", "five", "fixed", "fizzy", "flat", "fleet", "fluid", "foggy", "fond", "four", "free", "fresh", "front", "full", "funky", "funny", "furry", "fussy", "fuzzy", "gaudy", "giant", "glad", "gold", "good", "grand", "gray", "great", "green", "grim", "gross", "grown", "gusty", "hairy", "half", "happy", "hard", "hardy", "harsh", "hasty", "hazy", "hefty", "high", "holy", "honey", "hot", "huge", "humid", "husky", "icy", "ideal", "idle", "inner", "ionic", "iron", "ivory", "jade", "jazzy", "jolly", "juicy", "jumbo", "jumpy", "just", "keen", "kind", "known", "lanky", "large", "last", "late", "lazy", "lean", "legal", "light", "limp", "live", "local", "lofty", "lone", "long", "lost", "loud", "loved", "low", "loyal", "lucky", "lumpy", "lusty", "mad", "magic", "main", "major", "meek", "merry", "messy", "mild", "minty", "misty", "mixed", "moist", "moody", "mossy", "muddy", "murky", "mushy", "muted", "naive", "neat", "nerdy", "new", "next", "nice", "nimby", "noble", "noisy", "north", "novel", "numb", "nutty", "oaken", "odd", "oily", "old", "olive", "only", "open", "other", "outer", "oval", "paid", "pale", "pasty", "perky", "petty", "pink", "plain", "plump", "plush", "polar", "poor", "prime", "proud", "pulpy", "pure", "pushy", "quick", "quiet", "rare", "raw", "ready", "real", "rich", "rigid", "ripe", "rosy", "rough", "round", "royal", "ruby", "rusty", "safe", "salty", "same", "sandy", "sharp", "shiny", "silky", "slim", "slow", "small", "snug"];
15
+ export declare const nouns: readonly ["ant", "ape", "asp", "auk", "bass", "bat", "bear", "bee", "bird", "bison", "boar", "bream", "buck", "bug", "bull", "bunny", "calf", "carp", "cat", "chick", "chimp", "clam", "cobra", "cod", "colt", "conch", "coon", "cow", "crab", "crane", "crow", "cub", "dart", "deer", "dingo", "dodo", "doe", "dog", "dove", "drake", "drum", "duck", "eagle", "eel", "egret", "elk", "emu", "ewe", "fawn", "finch", "fish", "flea", "fly", "foal", "fox", "frog", "gator", "gecko", "goat", "goose", "grub", "gull", "guppy", "hare", "hawk", "hen", "heron", "hog", "hornet", "horse", "hound", "hyena", "ibex", "ibis", "iguana", "imp", "jackal", "jay", "joey", "kite", "kiwi", "koala", "koi", "lamb", "lark", "lemur", "lion", "llama", "lynx", "macaw", "mako", "mare", "mink", "mite", "mole", "moose", "moth", "mouse", "mule", "newt", "okapi", "orca", "oryx", "otter", "owl", "ox", "panda", "parrot", "perch", "pig", "pike", "plover", "pony", "prawn", "pug", "puma", "quail", "ram", "rat", "raven", "ray", "robin", "rook", "roach", "sail", "seal", "shad", "shark", "sheep", "shrew", "shrimp", "skate", "skink", "skua", "skunk", "sloth", "slug", "smelt", "snail", "snake", "snipe", "sole", "squid", "stag", "stork", "swan", "swift", "tapir", "tern", "thrush", "toad", "trout", "tuna", "turkey", "turtle", "viper", "vole", "vulture", "wasp", "whale", "wolf", "wombat", "worm", "wren", "yak", "zebra", "adder", "akita", "alpaca", "anole", "bongo", "camel", "civet", "coati", "coral", "corgi", "dhole", "drill", "dugong", "dunnit", "eland", "ermine", "falcon", "ferret", "gibbon", "gopher", "grouse", "haddok", "hermit", "hippo", "hoopoe", "hutia", "impala", "indri", "isopod", "jacana", "jerboa", "kakapo", "kudu", "loris", "magpie", "marten", "mayfly", "merlin", "murre", "nandu", "numbat", "ocelot", "osprey", "oyster", "paca", "pangol", "pariah", "peahen", "pipit", "pollock", "possum", "potoo", "python", "quokka", "rail", "redfin", "reebok", "remora", "rhea", "sable", "saola", "serval", "siskin", "snapper", "snoek", "sparrow", "spider", "sponge", "sprat", "stoat", "stilt", "stint", "sunbird", "tanuki", "tarpon", "tenrec", "tigon", "toucan", "treefr", "uguisu", "urutu", "vervet", "vizsla", "walrus", "weasel", "weevil", "whimbr", "whydah", "wisent", "zorilla"];
16
+ /**
17
+ * FNV-1a hash (32-bit). Fast, good distribution, zero dependencies.
18
+ * Reference: http://www.isthe.com/chongo/tech/comp/fnv/
19
+ */
20
+ export declare function fnv1a(str: string): number;
21
+ /**
22
+ * Map a string to a deterministic adjective-noun pair.
23
+ * Same input always produces the same word tuple.
24
+ */
25
+ export declare function toWordTuple(str: string): string;
package/dist/words.js ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Word lists for generating deterministic human-readable callpoint names.
3
+ * Used as a fallback in production when the gg-call-sites Vite plugin is not installed.
4
+ *
5
+ * Each list has exactly 256 entries (8-bit index) so a 32-bit hash cleanly maps to
6
+ * an adjective-noun pair: adjectives[hash & 0xFF] + "-" + nouns[(hash >> 8) & 0xFF]
7
+ * giving 65,536 unique combinations.
8
+ *
9
+ * Selection criteria:
10
+ * - Short (3-6 chars) for compact console output
11
+ * - Visually distinct (no near-homoglyphs like fast/last)
12
+ * - Inoffensive
13
+ */
14
+ // prettier-ignore
15
+ export const adjectives = [
16
+ 'able', 'acid', 'aged', 'airy', 'apt', 'avid', 'awry', 'balmy',
17
+ 'bare', 'beefy', 'bent', 'big', 'bland', 'bleak', 'blind', 'bliss',
18
+ 'blue', 'blunt', 'bold', 'brash', 'brave', 'brief', 'briny', 'brisk',
19
+ 'broad', 'buff', 'bulky', 'bumpy', 'burly', 'busy', 'calm', 'cheap',
20
+ 'chewy', 'chief', 'chilly', 'civil', 'clean', 'clear', 'close', 'cold',
21
+ 'cool', 'coral', 'cozy', 'crisp', 'cubic', 'curly', 'curvy', 'cute',
22
+ 'cyan', 'damp', 'dark', 'dear', 'deep', 'dense', 'dewy', 'dim',
23
+ 'dizzy', 'dopey', 'dorky', 'draft', 'dry', 'dual', 'dull', 'dusty',
24
+ 'eager', 'early', 'easy', 'edgy', 'elfin', 'elite', 'empty', 'equal',
25
+ 'even', 'every', 'evil', 'exact', 'extra', 'faded', 'fair', 'fancy',
26
+ 'fast', 'few', 'fine', 'firm', 'first', 'fishy', 'fit', 'five',
27
+ 'fixed', 'fizzy', 'flat', 'fleet', 'fluid', 'foggy', 'fond', 'four',
28
+ 'free', 'fresh', 'front', 'full', 'funky', 'funny', 'furry', 'fussy',
29
+ 'fuzzy', 'gaudy', 'giant', 'glad', 'gold', 'good', 'grand', 'gray',
30
+ 'great', 'green', 'grim', 'gross', 'grown', 'gusty', 'hairy', 'half',
31
+ 'happy', 'hard', 'hardy', 'harsh', 'hasty', 'hazy', 'hefty', 'high',
32
+ 'holy', 'honey', 'hot', 'huge', 'humid', 'husky', 'icy', 'ideal',
33
+ 'idle', 'inner', 'ionic', 'iron', 'ivory', 'jade', 'jazzy', 'jolly',
34
+ 'juicy', 'jumbo', 'jumpy', 'just', 'keen', 'kind', 'known', 'lanky',
35
+ 'large', 'last', 'late', 'lazy', 'lean', 'legal', 'light', 'limp',
36
+ 'live', 'local', 'lofty', 'lone', 'long', 'lost', 'loud', 'loved',
37
+ 'low', 'loyal', 'lucky', 'lumpy', 'lusty', 'mad', 'magic', 'main',
38
+ 'major', 'meek', 'merry', 'messy', 'mild', 'minty', 'misty', 'mixed',
39
+ 'moist', 'moody', 'mossy', 'muddy', 'murky', 'mushy', 'muted', 'naive',
40
+ 'neat', 'nerdy', 'new', 'next', 'nice', 'nimby', 'noble', 'noisy',
41
+ 'north', 'novel', 'numb', 'nutty', 'oaken', 'odd', 'oily', 'old',
42
+ 'olive', 'only', 'open', 'other', 'outer', 'oval', 'paid', 'pale',
43
+ 'pasty', 'perky', 'petty', 'pink', 'plain', 'plump', 'plush', 'polar',
44
+ 'poor', 'prime', 'proud', 'pulpy', 'pure', 'pushy', 'quick', 'quiet',
45
+ 'rare', 'raw', 'ready', 'real', 'rich', 'rigid', 'ripe', 'rosy',
46
+ 'rough', 'round', 'royal', 'ruby', 'rusty', 'safe', 'salty', 'same',
47
+ 'sandy', 'sharp', 'shiny', 'silky', 'slim', 'slow', 'small', 'snug',
48
+ ];
49
+ // prettier-ignore
50
+ export const nouns = [
51
+ 'ant', 'ape', 'asp', 'auk', 'bass', 'bat', 'bear', 'bee',
52
+ 'bird', 'bison', 'boar', 'bream', 'buck', 'bug', 'bull', 'bunny',
53
+ 'calf', 'carp', 'cat', 'chick', 'chimp', 'clam', 'cobra', 'cod',
54
+ 'colt', 'conch', 'coon', 'cow', 'crab', 'crane', 'crow', 'cub',
55
+ 'dart', 'deer', 'dingo', 'dodo', 'doe', 'dog', 'dove', 'drake',
56
+ 'drum', 'duck', 'eagle', 'eel', 'egret', 'elk', 'emu', 'ewe',
57
+ 'fawn', 'finch', 'fish', 'flea', 'fly', 'foal', 'fox', 'frog',
58
+ 'gator', 'gecko', 'goat', 'goose', 'grub', 'gull', 'guppy', 'hare',
59
+ 'hawk', 'hen', 'heron', 'hog', 'hornet', 'horse', 'hound', 'hyena',
60
+ 'ibex', 'ibis', 'iguana', 'imp', 'jackal', 'jay', 'joey', 'kite',
61
+ 'kiwi', 'koala', 'koi', 'lamb', 'lark', 'lemur', 'lion', 'llama',
62
+ 'lynx', 'macaw', 'mako', 'mare', 'mink', 'mite', 'mole', 'moose',
63
+ 'moth', 'mouse', 'mule', 'newt', 'okapi', 'orca', 'oryx', 'otter',
64
+ 'owl', 'ox', 'panda', 'parrot', 'perch', 'pig', 'pike', 'plover',
65
+ 'pony', 'prawn', 'pug', 'puma', 'quail', 'ram', 'rat', 'raven',
66
+ 'ray', 'robin', 'rook', 'roach', 'sail', 'seal', 'shad', 'shark',
67
+ 'sheep', 'shrew', 'shrimp', 'skate', 'skink', 'skua', 'skunk', 'sloth',
68
+ 'slug', 'smelt', 'snail', 'snake', 'snipe', 'sole', 'squid', 'stag',
69
+ 'stork', 'swan', 'swift', 'tapir', 'tern', 'thrush', 'toad', 'trout',
70
+ 'tuna', 'turkey', 'turtle', 'viper', 'vole', 'vulture', 'wasp', 'whale',
71
+ 'wolf', 'wombat', 'worm', 'wren', 'yak', 'zebra', 'adder', 'akita',
72
+ 'alpaca', 'anole', 'bongo', 'camel', 'civet', 'coati', 'coral', 'corgi',
73
+ 'dhole', 'drill', 'dugong', 'dunnit', 'eland', 'ermine', 'falcon', 'ferret',
74
+ 'gibbon', 'gopher', 'grouse', 'haddok', 'hermit', 'hippo', 'hoopoe', 'hutia',
75
+ 'impala', 'indri', 'isopod', 'jacana', 'jerboa', 'kakapo', 'kudu', 'loris',
76
+ 'magpie', 'marten', 'mayfly', 'merlin', 'murre', 'nandu', 'numbat', 'ocelot',
77
+ 'osprey', 'oyster', 'paca', 'pangol', 'pariah', 'peahen', 'pipit', 'pollock',
78
+ 'possum', 'potoo', 'python', 'quokka', 'rail', 'redfin', 'reebok', 'remora',
79
+ 'rhea', 'sable', 'saola', 'serval', 'siskin', 'snapper', 'snoek', 'sparrow',
80
+ 'spider', 'sponge', 'sprat', 'stoat', 'stilt', 'stint', 'sunbird', 'tanuki',
81
+ 'tarpon', 'tenrec', 'tigon', 'toucan', 'treefr', 'uguisu', 'urutu', 'vervet',
82
+ 'vizsla', 'walrus', 'weasel', 'weevil', 'whimbr', 'whydah', 'wisent', 'zorilla',
83
+ ];
84
+ /**
85
+ * FNV-1a hash (32-bit). Fast, good distribution, zero dependencies.
86
+ * Reference: http://www.isthe.com/chongo/tech/comp/fnv/
87
+ */
88
+ export function fnv1a(str) {
89
+ let hash = 0x811c9dc5; // FNV offset basis
90
+ for (let i = 0; i < str.length; i++) {
91
+ hash ^= str.charCodeAt(i);
92
+ hash = Math.imul(hash, 0x01000193); // FNV prime
93
+ }
94
+ return hash >>> 0; // Ensure unsigned 32-bit
95
+ }
96
+ /**
97
+ * Map a string to a deterministic adjective-noun pair.
98
+ * Same input always produces the same word tuple.
99
+ */
100
+ export function toWordTuple(str) {
101
+ const hash = fnv1a(str);
102
+ const adj = adjectives[hash & 0xff];
103
+ const noun = nouns[(hash >>> 8) & 0xff];
104
+ return `${adj}-${noun}`;
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leftium/gg",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Leftium/gg.git"