timepicker-ui-mcp 1.0.2 → 2.0.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.
@@ -0,0 +1,117 @@
1
+ import { API_METHODS, DOCS, THEMES, optionGroupDocs } from './data.js';
2
+ import { CSS_CLASSES, CSS_VARS, EVENTS, OPTION_GROUPS, TROUBLESHOOTING } from './generated.js';
3
+ function firstLine(text) {
4
+ return (text ?? '').split('\n')[0].trim();
5
+ }
6
+ function optionCandidates() {
7
+ const out = [];
8
+ const visit = (group, path, option) => {
9
+ const name = `${path}.${option.name}`;
10
+ out.push({
11
+ kind: 'option',
12
+ name,
13
+ summary: [option.type, firstLine(option.description)].filter(Boolean).join(' - '),
14
+ deprecated: option.deprecated,
15
+ docs: optionGroupDocs(group),
16
+ text: `${option.description ?? ''} ${option.type}`,
17
+ });
18
+ for (const field of option.fields ?? [])
19
+ visit(group, name, field);
20
+ };
21
+ for (const [group, data] of Object.entries(OPTION_GROUPS)) {
22
+ for (const option of data.options)
23
+ visit(group, group, option);
24
+ }
25
+ return out;
26
+ }
27
+ function candidates() {
28
+ return [
29
+ ...optionCandidates(),
30
+ ...EVENTS.filter((e) => !e.internal).map((e) => ({
31
+ kind: 'event',
32
+ name: e.name,
33
+ summary: `payload ${e.data}: ${e.fields.map((f) => f.name).join(', ') || 'none'}`,
34
+ deprecated: e.deprecated,
35
+ docs: DOCS.events,
36
+ text: `${e.description ?? ''} ${e.fields.map((f) => `${f.name} ${f.description ?? ''}`).join(' ')}`,
37
+ })),
38
+ ...CSS_VARS.map((v) => ({
39
+ kind: 'css-variable',
40
+ name: v.name,
41
+ summary: `${v.group}, default ${v.default ?? 'unset'}${v.read ? '' : ', no rule reads it'}`,
42
+ deprecated: v.read ? undefined : 'No rule reads it, so setting it changes nothing. Removed in 5.0.',
43
+ docs: DOCS.cssVariables,
44
+ text: v.group,
45
+ })),
46
+ ...CSS_CLASSES.classes.map((c) => ({
47
+ kind: 'css-class',
48
+ name: c.name,
49
+ summary: c.example ? `e.g. ${c.example}` : 'rendered by the templates',
50
+ docs: DOCS.cssClasses,
51
+ text: '',
52
+ })),
53
+ ...API_METHODS.map((m) => ({
54
+ kind: 'method',
55
+ name: m.signature,
56
+ summary: m.description,
57
+ deprecated: m.deprecated,
58
+ docs: DOCS.methods,
59
+ text: m.description,
60
+ })),
61
+ ...THEMES.map((t) => ({
62
+ kind: 'theme',
63
+ name: t.name,
64
+ summary: `import '${t.cssImport}'`,
65
+ docs: DOCS.themes,
66
+ text: t.note ?? '',
67
+ })),
68
+ ...TROUBLESHOOTING.map((t) => ({
69
+ kind: 'troubleshooting',
70
+ name: t.symptom,
71
+ summary: t.fix.map((step) => step.text).join(' '),
72
+ docs: t.docs,
73
+ text: `${t.group} ${t.why ?? ''} ${t.fix.map((step) => step.text).join(' ')}`,
74
+ })),
75
+ ];
76
+ }
77
+ let index;
78
+ export function searchReference(query, limit) {
79
+ const tokens = query
80
+ .toLowerCase()
81
+ .split(/[\s,]+/)
82
+ .map((t) => t.trim())
83
+ .filter((t) => t.length > 1);
84
+ if (tokens.length === 0)
85
+ return [];
86
+ index ??= candidates();
87
+ const scored = index
88
+ .map((item) => {
89
+ const name = item.name.toLowerCase();
90
+ const text = `${item.summary} ${item.text}`.toLowerCase();
91
+ let score = 0;
92
+ let matched = 0;
93
+ for (const token of tokens) {
94
+ let tokenScore = 0;
95
+ if (name === token || name.endsWith(`.${token}`))
96
+ tokenScore = 8;
97
+ else if (name.includes(token))
98
+ tokenScore = 4;
99
+ else if (text.includes(token))
100
+ tokenScore = 1;
101
+ if (tokenScore > 0)
102
+ matched += 1;
103
+ score += tokenScore;
104
+ }
105
+ if (matched === tokens.length)
106
+ score *= 2;
107
+ if (item.deprecated)
108
+ score /= 2;
109
+ return { item, score };
110
+ })
111
+ .filter((entry) => entry.score > 0)
112
+ .sort((a, b) => b.score - a.score || a.item.name.length - b.item.name.length);
113
+ return scored.slice(0, limit).map(({ item }) => {
114
+ const { text: _text, ...hit } = item;
115
+ return hit;
116
+ });
117
+ }
package/build/snippet.js CHANGED
@@ -18,6 +18,8 @@ function optionsFor(args) {
18
18
  if (args.mode && args.mode !== 'clock')
19
19
  ui.push(`mode: '${args.mode}'`);
20
20
  const clock = [`type: '${args.clockType}'`];
21
+ if (args.showSeconds)
22
+ clock.push('showSeconds: true');
21
23
  const groups = [];
22
24
  if (ui.length)
23
25
  groups.push(` ui: { ${ui.join(', ')} },`);
@@ -53,7 +55,11 @@ export function buildSnippet(args) {
53
55
  ' picker.create();',
54
56
  ' return () => picker.destroy();',
55
57
  ' }, []);',
56
- ' return <input ref={ref} />;',
58
+ ' return (',
59
+ ' <div className="tp-ui">',
60
+ ' <input ref={ref} type="text" />',
61
+ ' </div>',
62
+ ' );',
57
63
  '}',
58
64
  ].join('\n');
59
65
  }
package/build/theme.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CSS_VARS } from './generated.js';
2
+ export const CLASS_NAME = /^[a-z_][a-z0-9_-]*$/i;
2
3
  function normalizeVarName(key) {
3
4
  const trimmed = key.trim();
4
5
  if (trimmed.startsWith('--'))
@@ -9,32 +10,54 @@ function normalizeVarName(key) {
9
10
  }
10
11
  export function buildCustomTheme(args) {
11
12
  const known = new Map(CSS_VARS.map((v) => [v.name, v]));
12
- const base = args.onlyCommon ? CSS_VARS.filter((v) => v.commonlyThemed) : CSS_VARS;
13
- const overridePairs = Object.entries(args.overrides).map(([k, v]) => [normalizeVarName(k), v]);
14
- const overrideMap = new Map(overridePairs);
15
- const lines = [`[data-theme='${args.name}'] {`];
16
- if (overrideMap.size > 0) {
17
- for (const [name, value] of overrideMap) {
18
- const fallback = known.get(name);
19
- const suffix = fallback ? '' : ' /* not a known --tp- variable, verify the name */';
20
- lines.push(` ${name}: ${value};${suffix}`);
13
+ const scaffold = CSS_VARS.filter((v) => v.read && !v.readOutsideWrapper && (!args.onlyCommon || v.commonlyThemed));
14
+ const overrides = new Map(Object.entries(args.overrides).map(([k, v]) => [normalizeVarName(k), v]));
15
+ const wrapper = `.tp-ui-wrapper.${args.name}`;
16
+ const root = `.tp-ui.${args.name}`;
17
+ const modal = '.tp-ui-modal[data-theme]';
18
+ const blocks = new Map([[wrapper, []]]);
19
+ const add = (selector, line) => blocks.set(selector, [...(blocks.get(selector) ?? []), line]);
20
+ const warnings = [];
21
+ if (overrides.size > 0) {
22
+ for (const [name, value] of overrides) {
23
+ const info = known.get(name);
24
+ if (!info) {
25
+ warnings.push(`${name} is not a --tp- variable timepicker-ui defines. Check the name with list_css_variables.`);
26
+ add(wrapper, ` ${name}: ${value};`);
27
+ }
28
+ else if (!info.read) {
29
+ warnings.push(`${name} has no effect: no rule reads it, and 5.0 removes it. It was left out.`);
30
+ }
31
+ else if (info.readOutsideWrapper?.includes('tp-ui-modal')) {
32
+ add(modal, ` ${name}: ${value};`);
33
+ warnings.push(`${name} is read on .tp-ui-modal, outside .tp-ui-wrapper, so it is set there and applies to every picker.`);
34
+ }
35
+ else if (info.readOutsideWrapper) {
36
+ add(root, ` ${name}: ${value};`);
37
+ }
38
+ else {
39
+ add(wrapper, ` ${name}: ${value};`);
40
+ }
21
41
  }
22
42
  }
23
43
  else {
24
- for (const v of base) {
25
- lines.push(` ${v.name}: ${v.value};`);
26
- }
44
+ for (const v of scaffold)
45
+ add(wrapper, ` ${v.name}: ${v.default ?? 'initial'};`);
27
46
  }
28
- lines.push('}');
29
47
  const header = [
30
48
  `/* Custom timepicker-ui theme: "${args.name}"`,
31
49
  ' *',
32
- ' * 1. Save this CSS and import it in your app (after timepicker-ui/main.css).',
33
- " * 2. Pass ui.theme = '" + args.name + "' (cast as any in TS — custom names are not in the union).",
50
+ " * 1. Import this CSS after 'timepicker-ui/main.css' (and after a built-in theme file if you combine them).",
51
+ ` * 2. Pass the class: new TimepickerUI(input, { ui: { cssClass: '${args.name}' } }).`,
52
+ " * It can be combined with a built-in theme as the base: ui: { theme: 'dark', cssClass: '" + args.name + "' }.",
34
53
  ' *',
35
- " * The picker sets data-theme on its modal, so [data-theme='" + args.name + "'] scopes your overrides.",
54
+ ` * The picker adds the class to .tp-ui-wrapper when it opens, so .tp-ui-wrapper.${args.name} scopes these values.`,
36
55
  ' */',
37
56
  '',
38
57
  ].join('\n');
39
- return `${header}${lines.join('\n')}\n`;
58
+ const css = [...blocks]
59
+ .filter(([, lines]) => lines.length > 0)
60
+ .map(([selector, lines]) => `${selector} {\n${lines.join('\n')}\n}`)
61
+ .join('\n\n');
62
+ return { css: `${header}${css}\n`, warnings };
40
63
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "timepicker-ui-mcp",
3
- "version": "1.0.2",
3
+ "version": "2.0.0",
4
4
  "mcpName": "io.github.pglejzer/timepicker-ui-mcp",
5
- "description": "MCP server for timepicker-ui: exposes themes, UI modes, plugins, grouped options, events, the public API, config-snippet and custom-theme generation, and time validation as tools for Claude and other MCP clients.",
5
+ "description": "MCP server for timepicker-ui: options, events, CSS variables, themes, the public API, migration guides, setup snippets, custom themes, time validation and reference search for Claude and other MCP clients.",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "timepicker-ui-mcp": "build/index.js"
@@ -33,7 +33,7 @@
33
33
  "url": "https://github.com/pglejzer/timepicker-ui/issues"
34
34
  },
35
35
  "engines": {
36
- "node": ">=18"
36
+ "node": ">=20"
37
37
  },
38
38
  "publishConfig": {
39
39
  "access": "public"
@@ -48,10 +48,11 @@
48
48
  "inspect": "npx @modelcontextprotocol/inspector node build/index.js"
49
49
  },
50
50
  "dependencies": {
51
- "@modelcontextprotocol/sdk": "^1.12.0",
52
- "zod": "^3.23.8"
51
+ "@modelcontextprotocol/server": "^2.3.1",
52
+ "zod": "^4.6.0"
53
53
  },
54
54
  "devDependencies": {
55
+ "@modelcontextprotocol/client": "^2.3.1",
55
56
  "@types/node": "^25.9.2",
56
57
  "typescript": "^5.8.0"
57
58
  }